From 136a4ec0627d3d3afbca6e759c7bc562e7ec79ef Mon Sep 17 00:00:00 2001 From: ipa-lth Date: Fri, 27 Apr 2018 13:54:25 +0200 Subject: [PATCH 01/21] add canonical form 'modal' The respective unittest is weak, yet I did not envisioned somthing better one --- control/canonical.py | 74 +++++++++++++++++++++++++++++++-- control/tests/canonical_test.py | 30 +++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/control/canonical.py b/control/canonical.py index 5eacce372..8f1d54983 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -6,8 +6,8 @@ from .statesp import StateSpace from .statefbk import ctrb, obsv -from numpy import zeros, shape, poly -from numpy.linalg import solve, matrix_rank +from numpy import zeros, shape, poly, iscomplex, hstack +from numpy.linalg import solve, matrix_rank, eig __all__ = ['canonical_form', 'reachable_form', 'observable_form'] @@ -22,7 +22,7 @@ def canonical_form(xsys, form='reachable'): Canonical form for transformation. Chosen from: * 'reachable' - reachable canonical form * 'observable' - observable canonical form - * 'modal' - modal canonical form [not implemented] + * 'modal' - modal canonical form Returns ------- @@ -37,6 +37,8 @@ def canonical_form(xsys, form='reachable'): return reachable_form(xsys) elif form == 'observable': return observable_form(xsys) + elif form == 'modal': + return modal_form(xsys) else: raise ControlNotImplemented( "Canonical form '%s' not yet implemented" % form) @@ -139,3 +141,69 @@ def observable_form(xsys): zsys.B = Tzx * xsys.B return zsys, Tzx + +def modal_form(xsys): + """Convert a system into modal canonical form + + Parameters + ---------- + xsys : StateSpace object + System to be transformed, with state `x` + + Returns + ------- + zsys : StateSpace object + System in modal canonical form, with state `z` + T : matrix + Coordinate transformation: z = T * x + """ + # Check to make sure we have a SISO system + if not issiso(xsys): + raise ControlNotImplemented( + "Canonical forms for MIMO systems not yet supported") + + # Create a new system, starting with a copy of the old one + zsys = StateSpace(xsys) + + # Calculate eigenvalues and matrix of eigenvectors Tzx, + eigval, eigvec = eig(xsys.A) + + # Eigenvalues and according eigenvectors are not sorted, + # thus modal transformation is ambiguous + # Sorting eigenvalues and respective vectors by largest to smallest eigenvalue + idx = eigval.argsort()[::-1] + eigval = eigval[idx] + eigvec = eigvec[:,idx] + + # If all eigenvalues are real, the matrix of eigenvectors is Tzx directly + if not iscomplex(eigval).any(): + Tzx = eigvec + else: + # A is an arbitrary semisimple matrix + + # Keep track of complex conjugates (need only one) + lst_conjugates = [] + Tzx = None + for val, vec in zip(eigval, eigvec.T): + if iscomplex(val): + if val not in lst_conjugates: + lst_conjugates.append(val.conjugate()) + if Tzx is not None: + Tzx = hstack((Tzx, hstack((vec.real.T, vec.imag.T)))) + else: + Tzx = hstack((vec.real.T, vec.imag.T)) + else: + # if conjugate has already been seen, skip this eigenvalue + lst_conjugates.remove(val) + else: + if Tzx is not None: + Tzx = hstack((Tzx, vec.real.T)) + else: + Tzx = vec.real.T + + # Generate the system matrices for the desired canonical form + zsys.A = solve(Tzx, xsys.A).dot(Tzx) + zsys.B = solve(Tzx, xsys.B) + zsys.C = xsys.C.dot(Tzx) + + return zsys, Tzx diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index f5908a8f4..b21f98522 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -52,3 +52,33 @@ def test_unreachable_system(self): # Check if an exception is raised np.testing.assert_raises(ValueError, canonical_form, sys, "reachable") + + def test_modal_form(self): + """Test the modal canonical form""" + + # Create a system in the modal canonical form + A_true = np.diag([4.0, 3.0, 2.0, 1.0]) # order from the largest to the smallest + B_true = np.matrix("1.1 2.2 3.3 4.4").T + C_true = np.matrix("1.3 1.4 1.5 1.6") + D_true = 42.0 + + # Perform a coordinate transform with a random invertible matrix + T_true = np.matrix([[-0.27144004, -0.39933167, 0.75634684, 0.44135471], + [-0.74855725, -0.39136285, -0.18142339, -0.50356997], + [-0.40688007, 0.81416369, 0.38002113, -0.16483334], + [-0.44769516, 0.15654653, -0.50060858, 0.72419146]]) + 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 a state space system and convert it to the modal canonical form + 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?) + 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) From 708f8ecb4fbe6c99b775100afc0e91e4c502ad01 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 18:42:28 -0700 Subject: [PATCH 02/21] DOC: update support versions of python in setup.py --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6751f64cd..cd4bcbf9f 100644 --- a/setup.py +++ b/setup.py @@ -19,9 +19,8 @@ Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 -Programming Language :: Python :: 3.3 -Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 +Programming Language :: Python :: 3.6 Topic :: Software Development Topic :: Scientific/Engineering Operating System :: Microsoft :: Windows From 25f1371605397dad3c94c87bbe320d8a6831faa6 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 19:14:32 -0700 Subject: [PATCH 03/21] DOC: update installation instructions (#225) --- README.rst | 12 ++++++++++-- doc/intro.rst | 7 +++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 48b398bc8..7a30f2cb8 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Features Links ===== -- Project home page: http://python-control.sourceforge.net +- Project home page: http://python-control.org - Source code repository: https://github.com/python-control/python-control - Documentation: http://python-control.readthedocs.org/ - Issue tracker: https://github.com/python-control/python-control/issues @@ -46,7 +46,7 @@ https://github.com/python-control/Slycot Installation ============ -The package may be installed using pip or distutils. +The package may be installed using pip, conda, or distutils. Pip --- @@ -56,6 +56,14 @@ To install using pip:: pip install slycot # optional 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 + Distutils --------- diff --git a/doc/intro.rst b/doc/intro.rst index 7402576ba..4c2d62de3 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -62,11 +62,10 @@ For users with the Anaconda distribution of Python, the following commands can be used:: conda install numpy scipy matplotlib # if not yet installed - conda install -c python-control -c cyclus slycot control + conda install -c conda-forge control -This installs `slycot` and `python-control` from the `python-control` -channel and uses the `cyclus` channel to obtain the required `lapack` -package. +This installs `slycot` and `python-control` from conda-forge, including the +`openblas` package. Alternatively, to use setuptools, first `download the source `_ and unpack it. To install in your home directory, use:: From d3921db081ee1d486ac2625f88edac3a4873624b Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 19:15:14 -0700 Subject: [PATCH 04/21] DOC: fix indices on transfer function in doc/conventions (#227) --- doc/conventions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conventions.rst b/doc/conventions.rst index 048356436..89f3669ec 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -45,8 +45,8 @@ transfer functions .. math:: G(s) = \frac{\text{num}(s)}{\text{den}(s)} - = \frac{a_0 s^n + a_1 s^{n-1} + \cdots + a_n} - {b_0 s^m + b_1 s^{m-1} + \cdots + b_m}, + = \frac{a_0 s^m + a_1 s^{m-1} + \cdots + a_m} + {b_0 s^n + b_1 s^{n-1} + \cdots + b_n}, where n is generally greater than or equal to m (for a proper transfer function). From 4173dd58fddb3bd9810995c6fba267ad7dc11bbf Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 19:44:03 -0700 Subject: [PATCH 05/21] DOC: clarify frequency list argument in bode() docstring (#202) --- control/freqplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 33cf0ad61..80a37ce30 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -70,8 +70,8 @@ def bode_plot(syslist, omega=None, dB=None, Hz=None, deg=None, ---------- syslist : linsys List of linear input/output systems (single system is OK) - omega : freq_range - Range of frequencies in rad/sec + omega : list + List of frequencies in rad/sec to be used for frequency response dB : boolean If True, plot result in dB Hz : boolean From 1039edd91edc174e4fd044641c33c215f73fb18f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 21:16:04 -0700 Subject: [PATCH 06/21] DOC: update timebase explanations (#203) --- control/freqplot.py | 2 +- doc/conventions.rst | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 80a37ce30..f5761e282 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -106,7 +106,7 @@ 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 - time base. If not timebase is specified (dt = True), dt is set to 1. + timebase. If not timebase is specified (dt = True), dt is set to 1. Examples -------- diff --git a/doc/conventions.rst b/doc/conventions.rst index 89f3669ec..17567075e 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -77,9 +77,8 @@ performed. Discrete time systems --------------------- -By default, all systems are considered to be continuous time systems. A -discrete time system is created by specifying the 'time base' dt. The time -base argument can be given when a system is constructed: +A discrete time system is created by specifying a nonzero 'timebase', dt. +The timebase argument can be given when a system is constructed: * dt = None: no timebase specified * dt = 0: continuous time system @@ -89,11 +88,16 @@ base argument can be given when a system is constructed: Only the :class:`StateSpace` and :class:`TransferFunction` classes allow explicit representation of discrete time systems. -Systems must have the same time base in order to be combined. For -continuous time systems, the :func:`sample_system` function or the -:meth:`StateSpace.sample` and :meth:`TransferFunction.sample` methods can be -used to create a discrete time system from a continuous time system. See -:ref:`utility-and-conversions`. +Systems must have compatible timebases in order to be combined. A system +with timebase `None` can be combined with a system having a specified +timebase, and the result will have the timebase of the latter system. +Similarly, a discrete time system with unspecified sampling time (`dt = +True`) can be combined with a system having a specified sampling time, and +the result will be a discrete time system with the sample time of the latter +system. For continuous time systems, the :func:`sample_system` function or +the :meth:`StateSpace.sample` and :meth:`TransferFunction.sample` methods +can be used to create a discrete time system from a continuous time system. +See :ref:`utility-and-conversions`. Conversion between representations ---------------------------------- From 7ff494618573059c42c3de517d55650789102578 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 13 Jul 2018 21:44:55 -0700 Subject: [PATCH 07/21] DOC: fix small errors in docstring examples (#57) --- control/frdata.py | 4 ++-- control/xferfcn.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/control/frdata.py b/control/frdata.py index 8c4e1f2f6..d34200455 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -452,8 +452,8 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1): scalar, then the number of inputs and outputs can be specified manually, as in: - >>> sys = _convertToFRD(3.) # Assumes inputs = outputs = 1 - >>> sys = _convertToFRD(1., inputs=3, outputs=2) + >>> frd = _convertToFRD(3., omega) # Assumes inputs = outputs = 1 + >>> frd = _convertToFRD(1., omegs, inputs=3, outputs=2) In the latter example, sys's matrix transfer function is [[1., 1., 1.] [1., 1., 1.]]. diff --git a/control/xferfcn.py b/control/xferfcn.py index 50b9877fa..e8fe084f8 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1017,7 +1017,7 @@ def _convertToTransferFunction(sys, **kw): If sys is an array-like type, then it is converted to a constant-gain transfer function. - >>> sys = _convertToTransferFunction([[1. 0.], [2. 3.]]) + >>> sys = _convertToTransferFunction([[1., 0.], [2., 3.]]) In this example, the numerator matrix will be [[[1.0], [0.0]], [[2.0], [3.0]]] From 0a51c27bd1035bf938c64013158b814c613b19b5 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 14 Jul 2018 12:50:17 -0700 Subject: [PATCH 08/21] DOC: small documentation tweaks --- control/config.py | 6 +++++- control/lti.py | 22 +++++++++++++++++++++ control/statesp.py | 4 ++-- doc/README | 13 +++++++++---- doc/control.rst | 2 ++ doc/conventions.rst | 32 +++++++++++++++++------------- doc/index.rst | 3 ++- doc/intro.rst | 47 ++++++++++++++++++++++++--------------------- 8 files changed, 86 insertions(+), 43 deletions(-) diff --git a/control/config.py b/control/config.py index 98198aae7..10ab8a1ed 100644 --- a/control/config.py +++ b/control/config.py @@ -18,6 +18,8 @@ def use_matlab_defaults(): """ Use MATLAB compatible configuration settings + + The following conventions are used: * Bode plots plot gain in dB, phase in degrees, frequency in Hertz """ # Bode plot defaults @@ -28,7 +30,9 @@ def use_matlab_defaults(): # Set defaults to match FBS (Astrom and Murray) def use_fbs_defaults(): """ - Use `Astrom and Murray `_ 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 """ diff --git a/control/lti.py b/control/lti.py index 0bd05e902..5950d9d58 100644 --- a/control/lti.py +++ b/control/lti.py @@ -82,9 +82,21 @@ def isctime(self, strict=False): return self.dt == 0 def issiso(self): + '''Check to see if a system is single input, single output''' return self.inputs == 1 and self.outputs == 1 def damp(self): + '''Natural frequency, damping ratio of system poles + + Returns + ------- + wn : array + Natural frequencies for each system pole + zeta : array + Damping ratio for each system pole + poles : array + Array of system poles + ''' poles = self.pole() if isdtime(self, strict=True): @@ -102,6 +114,16 @@ def dcgain(self): # Test to see if a system is SISO def issiso(sys, strict=False): + """ + Check to see if a system is single input, single output + + Parameters + ---------- + sys : LTI system + System to be checked + strict: bool (default = False) + If strict is True, do not treat scalars as SISO + """ if isinstance(sys, (int, float, complex, np.number)) and not strict: return True elif not isinstance(sys, LTI): diff --git a/control/statesp.py b/control/statesp.py index 4a617abd2..b55a8c681 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1248,7 +1248,7 @@ def tf2ss(*args): def rss(states=1, outputs=1, inputs=1): """ - Create a stable **continuous** random state space object. + Create a stable *continuous* random state space object. Parameters ---------- @@ -1285,7 +1285,7 @@ def rss(states=1, outputs=1, inputs=1): def drss(states=1, outputs=1, inputs=1): """ - Create a stable **discrete** random state space object. + Create a stable *discrete* random state space object. Parameters ---------- diff --git a/doc/README b/doc/README index 9b829033e..f542badc5 100644 --- a/doc/README +++ b/doc/README @@ -1,7 +1,11 @@ Sphinx Documentation -------------------- +This directory contains the user manual for the python-control +toolbox. The documentation is built using sphinx. The master toctree +document is index.rst. -Note: Sphinx actually runs and imports python code, so broken code, or code not in conf.py sys.path, cannot be documented! +Note: Sphinx actually runs and imports python code, so broken code, or +code not in conf.py sys.path, cannot be documented! 1. Get Sphinx [http://sphinx.pocoo.org/] [python setup.py build/install] @@ -14,7 +18,8 @@ Note: Sphinx actually runs and imports python code, so broken code, or code not 4. >> touch *.rst >> make html [or make latex] -Creating/updating manual on sourceforge: - -5. >> rsync -rav _build/html/ user@shell.sourceforge.net:/home/project-web/python-control/htdocs/manual-N.mx/ +Creating/updating manual on readthedocs.org: +5. Log in to readthedocs.org and go to the 'Admin' menu for +python-control. Choose 'Versions' from the sidebar and mark the +latest release as 'Active'. Update the default version if needed. diff --git a/doc/control.rst b/doc/control.rst index 40cb407bb..1d0b14644 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -154,3 +154,5 @@ Utility functions and conversions timebase timebaseEqual unwrap + use_fbs_defaults + use_matlab_defaults diff --git a/doc/conventions.rst b/doc/conventions.rst index 17567075e..829a2a1a8 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -80,7 +80,7 @@ Discrete time systems A discrete time system is created by specifying a nonzero 'timebase', dt. The timebase argument can be given when a system is constructed: -* dt = None: no timebase specified +* dt = None: no timebase specified (default) * dt = 0: continuous time system * dt > 0: discrete time system with sampling period 'dt' * dt = True: discrete time with unspecified sampling period @@ -90,9 +90,9 @@ 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 -timebase, and the result will have the timebase of the latter system. +timebase; the result will have the timebase of the latter system. Similarly, a discrete time system with unspecified sampling time (`dt = -True`) can be combined with a system having a specified sampling time, and +True`) can be combined with a system having a specified sampling time; the result will be a discrete time system with the sample time of the latter system. For continuous time systems, the :func:`sample_system` function or the :meth:`StateSpace.sample` and :meth:`TransferFunction.sample` methods @@ -110,18 +110,23 @@ argument or using the explicit conversion functions :func:`ss2tf` and Time series data ================ - -This is a convention for function arguments and return values that -represent time series: sequences of values that change over time. It -is used throughout the library, for example in the functions +A variety of functions in the library return time series data: sequences of +values that change over time. A common set of conventions is used for +returning such data: columns represent different points in time, rows are +different components (e.g., inputs, outputs or states). For return +arguments, an array of times is given as the first returned argument, +followed by one or more arrays of variable values. This convention is used +throughout the library, for example in the functions :func:`forced_response`, :func:`step_response`, :func:`impulse_response`, and :func:`initial_response`. .. note:: - This convention is different from the convention used in the library - :mod:`scipy.signal`. In Scipy's convention the meaning of rows and columns - is interchanged. Thus, all 2D values must be transposed when they are - used with functions from :mod:`scipy.signal`. + The convention used by python-control is different from the convention + used in the `scipy.signal + `_ library. In + Scipy's convention the meaning of rows and columns is interchanged. + Thus, all 2D values must be transposed when they are used with functions + from `scipy.signal`_. Types: @@ -181,7 +186,7 @@ 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). Variables that can be configured, along with their default values: * bode_dB (False): Bode plot magnitude plotted in dB (otherwise powers of 10) @@ -194,6 +199,7 @@ Variables that can be configured, along with their default values: Functions that can be used to set standard configurations: .. autosummary:: - + :toctree: generated/ + use_fbs_defaults use_matlab_defaults diff --git a/doc/index.rst b/doc/index.rst index 407e89817..7ea8fe1dd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,7 +12,8 @@ implements basic operations for analysis and design of feedback control systems. - Time response: initial, step, impulse - Frequency response: Bode and Nyquist plots - Control analysis: stability, reachability, observability, stability margins -- Control design: eigenvalue placement, linear quadratic regulator +- Control design: eigenvalue placement, LQR, H2, Hinf +- Model reduction: balanced realizations, Hankel singular values - Estimator design: linear quadratic estimator (Kalman filter) .. rubric:: Documentation diff --git a/doc/intro.rst b/doc/intro.rst index 4c2d62de3..9677135c1 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -7,36 +7,41 @@ Manual. This manual contains information on using the python-control package, including documentation for all functions in the package and examples illustrating their use. -Overview of the Toolbox +Overview of the toolbox ======================= The python-control package is a set of python classes and functions that implement common operations for the analysis and design of feedback control systems. The initial goal is to implement all of the functionality required to work through the examples in the textbook `Feedback Systems -`_ by Astrom and Murray. A -MATLAB compatibility package (control.matlab) is available that provides -many of the common functions corresponding to commands available in the -MATLAB Control Systems Toolbox. +`_ by Astrom and Murray. A :ref:`matlab-module` is +available that provides many of the common functions corresponding to +commands available in the MATLAB Control Systems Toolbox. -Some Differences from MATLAB +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 +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: * You must include commas in vectors. So [1 2 3] must be [1, 2, 3]. -* Functions that return multiple arguments use tuples -* You cannot use braces for collections; use tuples instead +* Functions that return multiple arguments use tuples. +* You cannot use braces for collections; use tuples instead. Installation ============ -The `python-control` package may be installed using pip, conda or the -standard distutils/setuptools mechanisms. +The `python-control` package can be installed using pip, conda or the +standard distutils/setuptools mechanisms. The package requires `numpy`_ and +`scipy`_, and the plotting routines require `matplotlib +`_. In addition, some routines require the `slycot +`_ library in order to implement +more advanced features (including some MIMO functionality). + To install using pip:: @@ -54,9 +59,10 @@ correctly by running the command:: python -c "import slycot" and verifying that no error message appears. It may be necessary to install -`slycot` from source, which requires a working FORTRAN compiler and the -`lapack` library. More information on the slycot package can be obtained -from the `slycot project page `_. +`slycot` from source, which requires a working FORTRAN compiler and either +the `lapack` or `openplas` library. More information on the slycot package +can be obtained from the `slycot project page +`_. For users with the Anaconda distribution of Python, the following commands can be used:: @@ -67,8 +73,9 @@ commands can be used:: This installs `slycot` and `python-control` from conda-forge, including the `openblas` package. -Alternatively, to use setuptools, first `download the source `_ and unpack -it. To install in your home directory, use:: +Alternatively, to use setuptools, first `download the source +`_ and unpack it. +To install in your home directory, use:: python setup.py install --user @@ -77,11 +84,7 @@ or to install for all users (on Linux or Mac OS):: python setup.py build sudo python setup.py install -The package requires `numpy` and `scipy`, and the plotting routines require -`matplotlib`. In addition, some routines require the `slycot` module, -described above. - -Getting Started +Getting started =============== There are two different ways to use the package. For the default interface From 70bf20754cfc323df7914c983ea89cd37154dc50 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 29 Jul 2018 14:30:06 -0700 Subject: [PATCH 09/21] update statesp_test.py unit test to fix numpy warnings --- control/tests/statesp_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 35f511ab1..3e9adcca6 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -148,7 +148,11 @@ def testEvalFr(self): # Deprecated version of the call (should generate warning) import warnings with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + # 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) From 53e0a092377fc8a181016f732f94f7c26871443a Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 29 Jul 2018 14:47:51 -0700 Subject: [PATCH 10/21] update freqresp_test.py unit test to fix numpy warnings --- control/tests/freqresp_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index a6fc49a7f..609a91e8f 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -141,6 +141,11 @@ def test_discrete(self): import warnings warnings.simplefilter('always', UserWarning) # don't supress 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") + + # Look for a warning about sampling above Nyquist frequency omega_bad = np.linspace(10e-4,1.1,10) * np.pi/sys.dt ret = sys.freqresp(omega_bad) print("len(w) =", len(w)) From cc192e0763e94bdad5168ada1194c67803e20b11 Mon Sep 17 00:00:00 2001 From: Hermann von Kleist Date: Thu, 1 Nov 2018 14:20:11 +0100 Subject: [PATCH 11/21] Remove print statement from impulse_response For better usage in Jupyter Notebooks --- control/timeresp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/control/timeresp.py b/control/timeresp.py index 4c137c933..a0ecf9d93 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -592,7 +592,6 @@ def impulse_response(sys, T=None, X0=0., input=0, output=None, else: new_X0 = X0 U[0] = 1. - print("discrete", U) T, yout, _xout = forced_response( sys, T, U, new_X0, From b1c66701728f7be2071b6519e04d4b0b2abbc947 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 4 Nov 2018 15:32:27 -0800 Subject: [PATCH 12/21] fix issue #240: _common_den was not padding properly --- control/tests/convert_test.py | 23 +++++++++++++++++++++++ control/tests/timeresp_test.py | 18 ++++++++++++++++++ control/xferfcn.py | 19 ++++++++++++------- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 5d9012399..0340fa718 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -245,6 +245,29 @@ def testTf2SsDuplicatePoles(self): except ImportError: print("Slycot not present, skipping") + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_tf2ss_robustness(self): + """Unit test to make sure that tf2ss is working correctly. + Source: https://github.com/python-control/python-control/issues/240 + """ + import control + + num = [ [[0], [1]], [[1], [0]] ] + den1 = [ [[1], [1,1]], [[1,4], [1]] ] + sys1tf = control.tf(num, den1) + sys1ss = control.tf2ss(sys1tf) + + # slight perturbation + den2 = [ [[1], [1e-10, 1, 1]], [[1,4], [1]] ] + sys2tf = control.tf(num, den2) + sys2ss = control.tf2ss(sys2tf) + + # Make sure that the poles match for StateSpace and TransferFunction + np.testing.assert_array_almost_equal(np.sort(sys1tf.pole()), + np.sort(sys1ss.pole())) + np.testing.assert_array_almost_equal(np.sort(sys2tf.pole()), + np.sort(sys2ss.pole())) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestConvert) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 9a16e26a1..9e11a9d0c 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -15,6 +15,7 @@ from control.statesp import * from control.xferfcn import TransferFunction, _convertToTransferFunction from control.dtime import c2d +from control.exception import slycot_check class TestTimeresp(unittest.TestCase): def setUp(self): @@ -233,6 +234,23 @@ def test_discrete_initial(self): t, yout = impulse_response(h1, np.arange(4)) np.testing.assert_array_equal(yout[0], [0., 1., 0., 0.]) + @unittest.skipIf(not slycot_check(), "slycot not installed") + def test_step_robustness(self): + "Unit test: https://github.com/python-control/python-control/issues/240" + # Create 2 input, 2 output system + num = [ [[0], [1]], [[1], [0]] ] + + den1 = [ [[1], [1,1]], [[1,4], [1]] ] + sys1 = TransferFunction(num, den1) + + den2 = [ [[1], [1e-10, 1, 1]], [[1,4], [1]] ] # slight perturbation + sys2 = TransferFunction(num, den2) + + # Compute step response from input 1 to output 1, 2 + t1, y1 = step_response(sys1, input=0) + t2, y2 = step_response(sys2, input=0) + np.testing.assert_array_almost_equal(y1, y2) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestTimeresp) diff --git a/control/xferfcn.py b/control/xferfcn.py index e8fe084f8..de3998124 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -800,9 +800,13 @@ 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 + + # now create the numerator, also padded on the right for i in range(self.outputs): # start with the current set of zeros for this output nwzeros = list(poleset[i][j][0]) @@ -811,14 +815,15 @@ def _common_den(self, imag_tol=None): for ip in chain(poleset[i][j][3], range(poleset[i][j][4],np)): nwzeros.append(poles[j][ip]) - + numpoly = poleset[i][j][2] * polyfromroots(nwzeros).real - m = npmax - len(numpoly) - #print(j,i,m,len(numpoly),len(poles[j])) - if m < 0: - num[i,j,::-1] = numpoly - else: - num[i,j,:m:-1] = numpoly + # print(numpoly, den[j]) + # polyfromroots gives coeffs in opposite order => invert + # 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] + # print(num[i, j]) + if (abs(den.imag) > epsnm).any(): print("Warning: The denominator has a nontrivial imaginary part: %f" % abs(den.imag).max()) From 97f201b132988a5a842a860844e2df8ae297ab01 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Thu, 22 Nov 2018 10:55:14 +0800 Subject: [PATCH 13/21] implement indexing for transfer function --- control/tests/xferfcn_test.py | 23 ++++++++++++++++++-- control/xferfcn.py | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 204c6dfd8..0d208c6db 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -5,8 +5,8 @@ import unittest import numpy as np -from control.statesp import StateSpace, _convertToStateSpace -from control.xferfcn import TransferFunction, _convertToTransferFunction +from control.statesp import StateSpace, _convertToStateSpace, rss +from control.xferfcn import TransferFunction, _convertToTransferFunction, ss2tf from control.lti import evalfr from control.exception import slycot_check # from control.lti import isdtime @@ -536,6 +536,25 @@ def testMIMO(self): np.testing.assert_array_almost_equal(H.num[1][0], H2.num[1][0]) np.testing.assert_array_almost_equal(H.den[1][0], H2.den[1][0]) + def testIndexing(self): + tm = ss2tf(rss(5, 3, 3)) + + # scalar indexing + sys01 = tm[0, 1] + np.testing.assert_array_almost_equal(sys01.num[0][0], tm.num[0][1]) + np.testing.assert_array_almost_equal(sys01.den[0][0], tm.den[0][1]) + + # slice indexing + sys = tm[:2, 1:3] + np.testing.assert_array_almost_equal(sys.num[0][0], tm.num[0][1]) + np.testing.assert_array_almost_equal(sys.den[0][0], tm.den[0][1]) + np.testing.assert_array_almost_equal(sys.num[0][1], tm.num[0][2]) + np.testing.assert_array_almost_equal(sys.den[0][1], tm.den[0][2]) + np.testing.assert_array_almost_equal(sys.num[1][0], tm.num[1][1]) + np.testing.assert_array_almost_equal(sys.den[1][0], tm.den[1][1]) + np.testing.assert_array_almost_equal(sys.num[1][1], tm.num[1][2]) + np.testing.assert_array_almost_equal(sys.den[1][1], tm.den[1][2]) + def testMatrixMult(self): """MIMO transfer functions should be multiplyable by constant matrices""" diff --git a/control/xferfcn.py b/control/xferfcn.py index e8fe084f8..b8a48c0a7 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -491,6 +491,46 @@ def __pow__(self, other): if other < 0: return (TransferFunction([1], [1]) / self) * (self**(other+1)) + def __getitem__(self, key): + key1, key2 = key + + # pre-process + if isinstance(key1, int): + key1 = slice(key1, key1 + 1, 1) + if isinstance(key2, int): + key2 = slice(key2, key2 + 1, 1) + # dim1 + start1, stop1, step1 = key1.start, key1.stop, key1.step + if step1 is None: + step1 = 1 + if start1 is None: + start1 = 0 + if stop1 is None: + stop1 = len(self.num) + # dim1 + start2, stop2, step2 = key2.start, key2.stop, key2.step + if step2 is None: + step2 = 1 + if start2 is None: + start2 = 0 + if stop2 is None: + stop2 = len(self.num[0]) + + num = [] + den = [] + for i in range(start1, stop1, step1): + num_i = [] + den_i = [] + for j in range(start2, stop2, step2): + num_i.append(self.num[i][j]) + den_i.append(self.den[i][j]) + num.append(num_i) + den.append(den_i) + if self.isctime(): + return TransferFunction(num, den) + else: + return TransferFunction(num, den, self.dt) + def evalfr(self, omega): """Evaluate a transfer function at a single angular frequency. From 9a42fc05c9a11effa2394acb1329086bf112cb4e Mon Sep 17 00:00:00 2001 From: arnold Date: Sat, 24 Feb 2018 12:33:57 -0700 Subject: [PATCH 14/21] Add discrete time support to place_varga(). Also, fix issue #177 by changing how the alpha parameter is computed. In additional, expose alpha to the user an optional parameter --- control/statefbk.py | 49 +++++++++++++++++----- control/tests/statefbk_test.py | 75 +++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 12 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index a2d75c30d..9860c63bd 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -113,11 +113,11 @@ def place(A, B, p): return K -def place_varga(A, B, p): +def place_varga(A, B, p, DICO='C', alpha=None): """Place closed loop eigenvalues - K = place_varga(A, B, p) + K = place_varga(A, B, p, DICO='C', alpha=None) - Parameters + Required Parameters ---------- A : 2-d array Dynamics matrix @@ -125,6 +125,20 @@ def place_varga(A, B, p): Input matrix p : 1-d list Desired eigenvalue locations + + Optional Parameters + --------------- + DICO : 'C' for continuous time pole placement or 'D' for discrete time. + The default is DICO='C'. + alpha: double scalar + If DICO='C', then place_varga will leave the eigenvalues with real + real part less than alpha untouched. + If DICO='D', the place_varga will leave eigenvalues with modulus + less than alpha untouched. + + By default (alpha=None), place_varga computes alpha such that all + poles will be placed. + Returns ------- K : 2-d array @@ -160,24 +174,39 @@ def place_varga(A, B, p): raise ControlSlycot("can't find slycot module 'sb01bd'") # Convert the system inputs to NumPy arrays - A_mat = np.array(A); - B_mat = np.array(B); + A_mat = np.array(A) + B_mat = np.array(B) if (A_mat.shape[0] != A_mat.shape[1] or A_mat.shape[0] != B_mat.shape[0]): raise ControlDimension("matrix dimensions are incorrect") # Compute the system eigenvalues and convert poles to numpy array system_eigs = np.linalg.eig(A_mat)[0] - placed_eigs = np.array(p); + placed_eigs = np.array(p) - # SB01BD sets eigenvalues with real part less than alpha - # We want to place all poles of the system => set alpha to minimum - alpha = min(system_eigs.real); + if alpha is None: + # SB01BD ignores eigenvalues with real part less than alpha + # (if DICO='C') or with modulus less than alpha + # (if DICO = 'D'). + if DICO == 'C': + # Choosing alpha=min_eig is insufficient and can lead to an + # error or not having all the eigenvalues placed that we wanted. + # Evidently, what python thinks are the eigs is not precisely + # the same as what slicot thinks are the eigs. So we need some + # numerical breathing room. The following is pretty heuristic, + # but does the trick + alpha = -2*abs(min(system_eigs.real)) + elif DICO == 'D': + # For discrete time, slycot only cares about modulus, so just make + # alpha the smallest it can be. + alpha = 0.0 + elif DICO == 'D' and alpha < 0.0: + raise ValueError("Need alpha > 0 when DICO='D'") # Call SLICOT routine to place the eigenvalues A_z,w,nfp,nap,nup,F,Z = \ sb01bd(B_mat.shape[0], B_mat.shape[1], len(placed_eigs), alpha, - A_mat, B_mat, placed_eigs, 'C'); + A_mat, B_mat, placed_eigs, DICO) # Return the gain matrix, with MATLAB gain convention return -F diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 042bda701..88c5fc60b 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, lqr, gram, acker +from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker from control.matlab import * from control.exception import slycot_check, ControlDimension @@ -186,7 +186,10 @@ def testPlace(self): np.testing.assert_raises(ValueError, place, A, B, P_repeated) @unittest.skipIf(not slycot_check(), "slycot not installed") - def testPlace_varga(self): + def testPlace_varga_continuous(self): + """ + Check that we can place eigenvalues for DICO='C' + """ A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -202,6 +205,74 @@ def testPlace_varga(self): 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) + + 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) + + def testPlace_varga_discrete(self): + """ + Check that we can place poles using DICO='D' (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, DICO='D') + 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) + + 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, DICO='D', 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 From cfe18eabf1a7e8316d23cea8763679014f95d919 Mon Sep 17 00:00:00 2001 From: arnold Date: Sat, 24 Feb 2018 12:36:33 -0700 Subject: [PATCH 15/21] Add decorators to skip new tests for place_varga if slycot is not installed --- control/tests/statefbk_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 88c5fc60b..0f1583c08 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -218,6 +218,7 @@ def testPlace_varga_continuous(self): 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 @@ -239,6 +240,7 @@ def testPlace_varga_continuous_partial_eigs(self): 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 DICO='D' (discrete time) @@ -254,6 +256,7 @@ def testPlace_varga_discrete(self): 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 From 33c59a86a497d6d4dc05455573f1ecc94c0718fa Mon Sep 17 00:00:00 2001 From: arnold Date: Thu, 22 Nov 2018 15:44:07 -0700 Subject: [PATCH 16/21] Change DICO parameter to boolean dtime. Update tests as well. --- control/statefbk.py | 29 ++++++++++++++++++----------- control/tests/statefbk_test.py | 8 ++++---- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index 9860c63bd..0fb377a47 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -113,9 +113,9 @@ def place(A, B, p): return K -def place_varga(A, B, p, DICO='C', alpha=None): +def place_varga(A, B, p, dtime=False, alpha=None): """Place closed loop eigenvalues - K = place_varga(A, B, p, DICO='C', alpha=None) + K = place_varga(A, B, p, dtime=False, alpha=None) Required Parameters ---------- @@ -128,8 +128,8 @@ def place_varga(A, B, p, DICO='C', alpha=None): Optional Parameters --------------- - DICO : 'C' for continuous time pole placement or 'D' for discrete time. - The default is DICO='C'. + dtime: False for continuous time pole placement or True for discrete time. + The default is dtime=False. alpha: double scalar If DICO='C', then place_varga will leave the eigenvalues with real real part less than alpha untouched. @@ -160,7 +160,7 @@ def place_varga(A, B, p, DICO='C', alpha=None): -------- >>> A = [[-1, -1], [0, 1]] >>> B = [[0], [1]] - >>> K = place(A, B, [-2, -5]) + >>> K = place_varga(A, B, [-2, -5]) See Also: -------- @@ -184,11 +184,21 @@ def place_varga(A, B, p, DICO='C', alpha=None): system_eigs = np.linalg.eig(A_mat)[0] placed_eigs = np.array(p) + # Need a character parameter for SB01BD + if dtime: + DICO = 'D' + else: + DICO = 'C' + if alpha is None: # SB01BD ignores eigenvalues with real part less than alpha # (if DICO='C') or with modulus less than alpha # (if DICO = 'D'). - if DICO == 'C': + if dtime: + # For discrete time, slycot only cares about modulus, so just make + # alpha the smallest it can be. + alpha = 0.0 + else: # Choosing alpha=min_eig is insufficient and can lead to an # error or not having all the eigenvalues placed that we wanted. # Evidently, what python thinks are the eigs is not precisely @@ -196,13 +206,10 @@ def place_varga(A, B, p, DICO='C', alpha=None): # numerical breathing room. The following is pretty heuristic, # but does the trick alpha = -2*abs(min(system_eigs.real)) - elif DICO == 'D': - # For discrete time, slycot only cares about modulus, so just make - # alpha the smallest it can be. - alpha = 0.0 - elif DICO == 'D' and alpha < 0.0: + elif dtime and alpha < 0.0: raise ValueError("Need alpha > 0 when DICO='D'") + # Call SLICOT routine to place the eigenvalues A_z,w,nfp,nap,nup,F,Z = \ sb01bd(B_mat.shape[0], B_mat.shape[1], len(placed_eigs), alpha, diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 0f1583c08..35df769a2 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -188,7 +188,7 @@ def testPlace(self): @unittest.skipIf(not slycot_check(), "slycot not installed") def testPlace_varga_continuous(self): """ - Check that we can place eigenvalues for DICO='C' + Check that we can place eigenvalues for dtime=False """ A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -243,13 +243,13 @@ def testPlace_varga_continuous_partial_eigs(self): @unittest.skipIf(not slycot_check(), "slycot not installed") def testPlace_varga_discrete(self): """ - Check that we can place poles using DICO='D' (discrete time) + 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, DICO='D') + 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() @@ -269,7 +269,7 @@ def testPlace_varga_discrete_partial_eigs(self): P = np.array([0.2, 0.6]) P_expected = np.array([0.5, 0.6]) alpha = 0.51 - K = place_varga(A, B, P, DICO='D', alpha=alpha) + 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() From 8d64dd16c911d37a73d80ddfb2c7ea33cc9db5df Mon Sep 17 00:00:00 2001 From: "Andrew D. McGuire" Date: Thu, 20 Dec 2018 10:25:33 -0500 Subject: [PATCH 17/21] Fixing bug in xferfcn._evalfr that caused ValueError for discrete systems when list of angular frequencies contained more than one element --- control/xferfcn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index e8fe084f8..135191aab 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -510,7 +510,7 @@ def _evalfr(self, omega): # Convert the frequency to discrete time dt = timebase(self) s = exp(1.j * omega * dt) - if (omega * dt > pi): + if np.any(omega * dt > pi): warn("_evalfr: frequency evaluation above Nyquist frequency") else: s = 1.j * omega From 0a28c51f54d5f0c002f0de287844b9d9a553e149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Thu, 20 Dec 2018 23:48:24 -0200 Subject: [PATCH 18/21] Removed a doubled 'between' in text. --- doc/conventions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conventions.rst b/doc/conventions.rst index 829a2a1a8..7bdf3c628 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -15,7 +15,7 @@ LTI system representation Linear time invariant (LTI) systems are represented in python-control in state space, transfer function, or frequency response data (FRD) form. Most functions in the toolbox will operate on any of these data types and -functions for converting between between compatible types is provided. +functions for converting between compatible types is provided. State space systems ------------------- From 84681cd459f88ead01cd4656add9042ab899aca8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 20 Dec 2018 22:24:43 -0800 Subject: [PATCH 19/21] TRV: docstring fix --- control/statesp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/statesp.py b/control/statesp.py index b55a8c681..6385642eb 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -385,7 +385,7 @@ def evalfr(self, omega): with input value s = i * omega. """ - warn("StateSpace.evalfr(omega) will be depracted in a future " + warn("StateSpace.evalfr(omega) will be deprecated in a future " "release of python-control; use evalfr(sys, omega*1j) instead", PendingDeprecationWarning) return self._evalfr(omega) From cc4508c82c8b12df74601c13ac0c6cfaf78b4b40 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 20 Dec 2018 22:25:20 -0800 Subject: [PATCH 20/21] fix bad lqr test case (cross term not pos def) --- control/tests/matlab_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index efde21c1d..d187f6125 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -414,6 +414,14 @@ def testAcker(self): @unittest.skipIf(not slycot_check(), "slycot not installed") def testLQR(self): (K, S, E) = lqr(self.siso_ss1.A, self.siso_ss1.B, np.eye(2), np.eye(1)) + + # Should work if [Q N;N' R] is positive semi-definite + (K, S, E) = lqr(self.siso_ss2.A, self.siso_ss2.B, 10*np.eye(3), \ + np.eye(1), [[1], [1], [2]]) + + @unittest.skip("check not yet implemented") + def testLQR_checks(self): + # Make sure we get a warning if [Q N;N' R] is not positive semi-definite (K, S, E) = lqr(self.siso_ss2.A, self.siso_ss2.B, np.eye(3), \ np.eye(1), [[1], [1], [2]]) From fcb2af0ff0a7527cfa9e752ae085f363d469463c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 21 Dec 2018 15:02:42 -0800 Subject: [PATCH 21/21] skip TF indexing test if no slycot --- control/tests/xferfcn_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 0d208c6db..44953e731 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -536,6 +536,7 @@ def testMIMO(self): np.testing.assert_array_almost_equal(H.num[1][0], H2.num[1][0]) np.testing.assert_array_almost_equal(H.den[1][0], H2.den[1][0]) + @unittest.skipIf(not slycot_check(), "slycot not installed") def testIndexing(self): tm = ss2tf(rss(5, 3, 3))