From e8e87a47e045739c08aea6c7e0dc91eb1ee24b7e Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 May 2022 22:44:56 -0500 Subject: [PATCH 01/20] Add passivity module, is_passive function, and passivity_test. --- control/passivity.py | 54 +++++++++++++++++++++++++++++++++ control/tests/passivity_test.py | 25 +++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 control/passivity.py create mode 100644 control/tests/passivity_test.py diff --git a/control/passivity.py b/control/passivity.py new file mode 100644 index 000000000..f33f7ff09 --- /dev/null +++ b/control/passivity.py @@ -0,0 +1,54 @@ +''' +Author: Mark Yeatman +Date: May 15, 2022 +''' + +from . import statesp as ss +from sympy import symbols, Matrix, symarray +from lmi_sdp import LMI_NSD, to_cvxopt +from cvxopt import solvers + +import numpy as np + + +def is_passive(sys): + ''' + Indicates if a linear time invarient system is passive + + Constructs a linear matrix inequality and a feasibility optimization + such that is a solution exists, the system is passive. + + The source for the algorithm is: + McCourt, Michael J., and Panos J. Antsaklis. "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). + ''' + + A = sys.A + B = sys.B + C = sys.C + D = sys.D + + P = Matrix(symarray('p', A.shape)) + + # enforce symmetry in P + size = A.shape[0] + for i in range(0, size): + for j in range(0, size): + P[i, j] = P[j, i] + + # construct matrix for storage function x'*V*x + V = Matrix.vstack( + Matrix.hstack(A.T * P + P*A, P*B - C.T), + Matrix.hstack(B.T*P - C, Matrix(-D - D.T)) + ) + + # construct LMI, convert to form for feasibility solver + LMI_passivty = LMI_NSD(V, 0*V) + min_obj = 0 * symbols("x") + variables = V.free_symbols + solvers.options['show_progress'] = False + c, Gs, hs = to_cvxopt(min_obj, LMI_passivty, variables) + + # crunch feasibility solution + sol = solvers.sdp(c, Gs=Gs, hs=hs) + + return (sol["x"] is not None) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py new file mode 100644 index 000000000..5041c4695 --- /dev/null +++ b/control/tests/passivity_test.py @@ -0,0 +1,25 @@ +''' +Author: Mark Yeatman +Date: May 30, 2022 +''' + +import pytest +import numpy +from control import ss, passivity +from sympy import Matrix + + +def test_is_passive(): + A = numpy.array([[0, 1], [-2, -2]]) + B = numpy.array([[0], [1]]) + C = numpy.array([[-1, 2]]) + D = numpy.array([[1.5]]) + sys = ss(A, B, C, D) + + assert(passivity.is_passive(sys)) + + D = -D + sys = ss(A, B, C, D) + + assert(not passivity.is_passive(sys)) + From 2eef6612a046b2982327a36ffe03beb4a0aa54f3 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 6 Jun 2022 14:32:02 -0500 Subject: [PATCH 02/20] Remove dependancies on lmi-sdp and sympy for is_passive. --- control/passivity.py | 53 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/control/passivity.py b/control/passivity.py index f33f7ff09..12c2c8ad7 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -4,11 +4,8 @@ ''' from . import statesp as ss -from sympy import symbols, Matrix, symarray -from lmi_sdp import LMI_NSD, to_cvxopt -from cvxopt import solvers - import numpy as np +import cvxopt as cvx def is_passive(sys): @@ -27,28 +24,38 @@ def is_passive(sys): C = sys.C D = sys.D - P = Matrix(symarray('p', A.shape)) - - # enforce symmetry in P - size = A.shape[0] - for i in range(0, size): - for j in range(0, size): - P[i, j] = P[j, i] - - # construct matrix for storage function x'*V*x - V = Matrix.vstack( - Matrix.hstack(A.T * P + P*A, P*B - C.T), - Matrix.hstack(B.T*P - C, Matrix(-D - D.T)) + def make_LMI_matrix(P): + V = np.vstack(( + np.hstack((A.T @ P + P@A, P@B)), + np.hstack((B.T@P, np.zeros_like(D)))) + ) + return V + + P = np.zeros_like(A) + matrix_list = [] + state_space_size = A.shape[0] + for i in range(0, state_space_size): + for j in range(0, state_space_size): + if j <= i: + P = P*0.0 + P[i, j] = 1.0 + P[j, i] = 1.0 + matrix_list.append(make_LMI_matrix(P).flatten()) + + coefficents = np.vstack(matrix_list).T + + constants = -np.vstack(( + np.hstack((np.zeros_like(A), - C.T)), + np.hstack((- C, -D - D.T))) ) - # construct LMI, convert to form for feasibility solver - LMI_passivty = LMI_NSD(V, 0*V) - min_obj = 0 * symbols("x") - variables = V.free_symbols - solvers.options['show_progress'] = False - c, Gs, hs = to_cvxopt(min_obj, LMI_passivty, variables) + number_of_opt_vars = int( + (state_space_size**2-state_space_size)/2 + state_space_size) + c = cvx.matrix(0.0, (number_of_opt_vars, 1)) # crunch feasibility solution - sol = solvers.sdp(c, Gs=Gs, hs=hs) + sol = cvx.solvers.sdp(c, + Gs=[cvx.matrix(coefficents)], + hs=[cvx.matrix(constants)]) return (sol["x"] is not None) From 147a24ea0ba9da03b3774b7993e20e785776e027 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 6 Jun 2022 14:37:53 -0500 Subject: [PATCH 03/20] Use sys.nstates in stead of using A.shape[0] --- control/passivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index 12c2c8ad7..2bc00f958 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -33,7 +33,7 @@ def make_LMI_matrix(P): P = np.zeros_like(A) matrix_list = [] - state_space_size = A.shape[0] + state_space_size = sys.nstates for i in range(0, state_space_size): for j in range(0, state_space_size): if j <= i: From fdb2d4ad6bf15d7565347528a69a99ce4e121fda Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 6 Jun 2022 14:58:51 -0500 Subject: [PATCH 04/20] Attempt to setup cvxopt similar to slycot. --- .github/workflows/python-package-conda.yml | 4 ++++ control/exception.py | 12 ++++++++++++ control/tests/conftest.py | 2 ++ control/tests/passivity_test.py | 4 ++-- setup.py | 3 ++- 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 3f1910697..e4b1aea42 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -13,6 +13,7 @@ jobs: python-version: [3.7, 3.9] slycot: ["", "conda"] pandas: [""] + cvxopt: ["conda"] array-and-matrix: [0] include: - python-version: 3.9 @@ -46,6 +47,9 @@ jobs: if [[ '${{matrix.pandas}}' == 'conda' ]]; then conda install -c conda-forge pandas fi + if [[ '${{matrix.cvxopt}}' == 'conda' ]]; then + conda install -c conda-forge cvxopt + fi - name: Test with pytest env: diff --git a/control/exception.py b/control/exception.py index f66eb7f30..d14ff87e6 100644 --- a/control/exception.py +++ b/control/exception.py @@ -84,3 +84,15 @@ def pandas_check(): except: pandas_installed = False return pandas_installed + +# Utility function to see if cvxopt is installed +cvxopt_installed = None +def cvxopt_check(): + global cvxopt_installed + if cvxopt_installed is None: + try: + import cvxopt + cvxopt_installed = True + except: + cvxopt_installed = False + return cvxopt_installed \ No newline at end of file diff --git a/control/tests/conftest.py b/control/tests/conftest.py index b67ef3674..853c1dd61 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -18,6 +18,8 @@ # pytest.param(marks=) slycotonly = pytest.mark.skipif(not control.exception.slycot_check(), reason="slycot not installed") +cvxoptonly = pytest.mark.skipif(not control.exception.cvxopt_check(), + reason="cvxopt not installed") noscipy0 = pytest.mark.skipif(StrictVersion(sp.__version__) < "1.0", reason="requires SciPy 1.0 or greater") nopython2 = pytest.mark.skipif(sys.version_info < (3, 0), diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 5041c4695..182e3923f 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -3,12 +3,12 @@ Date: May 30, 2022 ''' -import pytest import numpy from control import ss, passivity from sympy import Matrix +from control.tests.conftest import cvxoptonly - +@cvxoptonly def test_is_passive(): A = numpy.array([[0, 1], [-2, -2]]) B = numpy.array([[0], [1]]) diff --git a/setup.py b/setup.py index f5e766ebb..58e4d11cf 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ 'matplotlib'], extras_require={ 'test': ['pytest', 'pytest-timeout'], - 'slycot': [ 'slycot>=0.4.0' ] + 'slycot': [ 'slycot>=0.4.0' ], + 'cvxopt': [ 'cvxopt>=1.2.0' ] } ) From dc46f3cf2c70a884661a1dbd2fa274ee99083c87 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 7 Jun 2022 11:48:36 -0400 Subject: [PATCH 05/20] Remove unused import. --- control/passivity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index 2bc00f958..f37443722 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -3,7 +3,6 @@ Date: May 15, 2022 ''' -from . import statesp as ss import numpy as np import cvxopt as cvx From 0ecc1354765cc7753d8a980ea2e70c9be79186b6 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 7 Jun 2022 11:49:47 -0400 Subject: [PATCH 06/20] Apply suggestions from code review Co-authored-by: Ben Greiner --- .github/workflows/python-package-conda.yml | 2 +- control/exception.py | 2 +- control/tests/passivity_test.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index e4b1aea42..c0e720ea8 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -13,7 +13,7 @@ jobs: python-version: [3.7, 3.9] slycot: ["", "conda"] pandas: [""] - cvxopt: ["conda"] + cvxopt: ["", "conda"] array-and-matrix: [0] include: - python-version: 3.9 diff --git a/control/exception.py b/control/exception.py index d14ff87e6..575c78c0a 100644 --- a/control/exception.py +++ b/control/exception.py @@ -95,4 +95,4 @@ def cvxopt_check(): cvxopt_installed = True except: cvxopt_installed = False - return cvxopt_installed \ No newline at end of file + return cvxopt_installed diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 182e3923f..947b5729c 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -5,7 +5,6 @@ import numpy from control import ss, passivity -from sympy import Matrix from control.tests.conftest import cvxoptonly @cvxoptonly From 649b21ee7e915fc8758413da04f4ec411ba58d61 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 7 Jun 2022 12:07:45 -0400 Subject: [PATCH 07/20] Update passivity.py --- control/passivity.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/control/passivity.py b/control/passivity.py index f37443722..17a64120d 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -4,8 +4,11 @@ ''' import numpy as np -import cvxopt as cvx +try: + import cvxopt as cvx +except ImportError as e: + cvx = None def is_passive(sys): ''' @@ -17,7 +20,9 @@ def is_passive(sys): The source for the algorithm is: McCourt, Michael J., and Panos J. Antsaklis. "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). ''' - + if cvx is None: + raise ModuleNotFoundError + A = sys.A B = sys.B C = sys.C From b73e6fe0eac04fc541056d3ee9430c767b6d126b Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 14:19:36 -0500 Subject: [PATCH 08/20] Address some review comments. --- .github/workflows/python-package-conda.yml | 2 +- control/passivity.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index c0e720ea8..d8f810104 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: test-linux: - name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.pandas && ', with pandas' || '' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }} + name: Python ${{ matrix.python-version }}${{ matrix.slycot && format(' with Slycot from {0}', matrix.slycot) || ' without Slycot' }}${{ matrix.pandas && ', with pandas' || '' }}${{ matrix.array-and-matrix == 1 && ', array and matrix' || '' }}${{ matrix.cvxopt && format(' with cvxopt from {0}', matrix.cvxopt) || ' without cvxopt' }} runs-on: ubuntu-latest strategy: diff --git a/control/passivity.py b/control/passivity.py index 17a64120d..109a75356 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -10,6 +10,7 @@ except ImportError as e: cvx = None + def is_passive(sys): ''' Indicates if a linear time invarient system is passive @@ -18,11 +19,13 @@ def is_passive(sys): such that is a solution exists, the system is passive. The source for the algorithm is: - McCourt, Michael J., and Panos J. Antsaklis. "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). + McCourt, Michael J., and Panos J. Antsaklis. + "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). ''' if cvx is None: + print("cvxopt required for passivity module") raise ModuleNotFoundError - + A = sys.A B = sys.B C = sys.C @@ -35,13 +38,12 @@ def make_LMI_matrix(P): ) return V - P = np.zeros_like(A) matrix_list = [] state_space_size = sys.nstates for i in range(0, state_space_size): for j in range(0, state_space_size): if j <= i: - P = P*0.0 + P = np.zeros_like(A) P[i, j] = 1.0 P[j, i] = 1.0 matrix_list.append(make_LMI_matrix(P).flatten()) From cd7ec0fce4cc73954d2c3769adff0373b9ef62b2 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 14:56:35 -0500 Subject: [PATCH 09/20] Update control/passivity.py Co-authored-by: Ben Greiner --- control/passivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index 109a75356..3d376f392 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -24,7 +24,7 @@ def is_passive(sys): ''' if cvx is None: print("cvxopt required for passivity module") - raise ModuleNotFoundError + raise ModuleNotFoundError("cvxopt required for passivity module") A = sys.A B = sys.B From d65f1d230baa84cf3c74378a1505f6eea5093439 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 15:01:52 -0500 Subject: [PATCH 10/20] Remove duplicate "print" statement. --- control/passivity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index 3d376f392..a87e07730 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -23,7 +23,6 @@ def is_passive(sys): "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). ''' if cvx is None: - print("cvxopt required for passivity module") raise ModuleNotFoundError("cvxopt required for passivity module") A = sys.A From 7e79c822d069ba13c77c210c4a9975168bea3b3b Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 15:36:56 -0500 Subject: [PATCH 11/20] Fix grammar in doc string. --- control/passivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index a87e07730..df99ee20c 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -16,7 +16,7 @@ def is_passive(sys): Indicates if a linear time invarient system is passive Constructs a linear matrix inequality and a feasibility optimization - such that is a solution exists, the system is passive. + such that if a solution exists, the system is passive. The source for the algorithm is: McCourt, Michael J., and Panos J. Antsaklis. From c57b928528df7573d47651abc8db9043a2e26d5c Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 17:08:48 -0500 Subject: [PATCH 12/20] Another grammar in doc string fix. --- control/passivity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/passivity.py b/control/passivity.py index df99ee20c..ad032d60b 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -13,7 +13,7 @@ def is_passive(sys): ''' - Indicates if a linear time invarient system is passive + Indicates if a linear time invariant system is passive Constructs a linear matrix inequality and a feasibility optimization such that if a solution exists, the system is passive. From 27487a9707d0ea28e41358f36e815eff792a2990 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 13 Jun 2022 19:39:29 -0500 Subject: [PATCH 13/20] Address edge case of stricly proper systems. --- control/passivity.py | 6 ++++++ control/tests/passivity_test.py | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/control/passivity.py b/control/passivity.py index ad032d60b..1d981c9de 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -10,6 +10,7 @@ except ImportError as e: cvx = None +lmi_epsilon = 1e-12 def is_passive(sys): ''' @@ -30,6 +31,10 @@ def is_passive(sys): C = sys.C D = sys.D + #account for strictly proper systems + [n,m] = D.shape + D = D + np.nextafter(0,1)*np.eye(n,m) + def make_LMI_matrix(P): V = np.vstack(( np.hstack((A.T @ P + P@A, P@B)), @@ -59,6 +64,7 @@ def make_LMI_matrix(P): c = cvx.matrix(0.0, (number_of_opt_vars, 1)) # crunch feasibility solution + cvx.solvers.options['show_progress'] = False sol = cvx.solvers.sdp(c, Gs=[cvx.matrix(coefficents)], hs=[cvx.matrix(constants)]) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 947b5729c..79197866f 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -15,10 +15,22 @@ def test_is_passive(): D = numpy.array([[1.5]]) sys = ss(A, B, C, D) + # happy path is passive assert(passivity.is_passive(sys)) + # happy path not passive D = -D sys = ss(A, B, C, D) assert(not passivity.is_passive(sys)) + #edge cases of D=0 boundary condition + B *= 0 + C *= 0 + D *= 0 + sys = ss(A, B, C, D) + assert(passivity.is_passive(sys)) + + A = A*1e12 + sys = ss(A, B, C, D) + assert(passivity.is_passive(sys)) \ No newline at end of file From d6916c661a7799e5998f84f5e2d34368ace528a8 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 14 Jun 2022 06:46:56 -0500 Subject: [PATCH 14/20] Expand unit tests, add info to doc string for parameters and returns, rename is_passive to ispassive for naming convention consistency. Autoformat to pep8. --- control/passivity.py | 21 +++++++++++----- control/tests/passivity_test.py | 43 +++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/control/passivity.py b/control/passivity.py index 1d981c9de..b00cc6b65 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -10,11 +10,10 @@ except ImportError as e: cvx = None -lmi_epsilon = 1e-12 -def is_passive(sys): +def ispassive(sys): ''' - Indicates if a linear time invariant system is passive + Indicates if a linear time invariant (LTI) system is passive Constructs a linear matrix inequality and a feasibility optimization such that if a solution exists, the system is passive. @@ -22,6 +21,16 @@ def is_passive(sys): The source for the algorithm is: McCourt, Michael J., and Panos J. Antsaklis. "Demonstrating passivity and dissipativity using computational methods." ISIS 8 (2013). + + Parameters + ---------- + sys: A continuous LTI system + System to be checked. + + Returns + ------- + bool: + The input system passive. ''' if cvx is None: raise ModuleNotFoundError("cvxopt required for passivity module") @@ -31,9 +40,9 @@ def is_passive(sys): C = sys.C D = sys.D - #account for strictly proper systems - [n,m] = D.shape - D = D + np.nextafter(0,1)*np.eye(n,m) + # account for strictly proper systems + [n, m] = D.shape + D = D + np.nextafter(0, 1)*np.eye(n, m) def make_LMI_matrix(P): V = np.vstack(( diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 79197866f..171d3c542 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -7,8 +7,9 @@ from control import ss, passivity from control.tests.conftest import cvxoptonly + @cvxoptonly -def test_is_passive(): +def test_ispassive(): A = numpy.array([[0, 1], [-2, -2]]) B = numpy.array([[0], [1]]) C = numpy.array([[-1, 2]]) @@ -16,21 +17,47 @@ def test_is_passive(): sys = ss(A, B, C, D) # happy path is passive - assert(passivity.is_passive(sys)) + assert(passivity.ispassive(sys)) # happy path not passive D = -D sys = ss(A, B, C, D) - assert(not passivity.is_passive(sys)) + assert(not passivity.ispassive(sys)) + + +@cvxoptonly +def test_ispassive_edge_cases(): + A = numpy.array([[0, 1], [-2, -2]]) + B = numpy.array([[0], [1]]) + C = numpy.array([[-1, 2]]) + D = numpy.array([[1.5]]) - #edge cases of D=0 boundary condition - B *= 0 - C *= 0 D *= 0 + + # strictly proper sys = ss(A, B, C, D) - assert(passivity.is_passive(sys)) + assert(passivity.ispassive(sys)) + # ill conditioned A = A*1e12 sys = ss(A, B, C, D) - assert(passivity.is_passive(sys)) \ No newline at end of file + assert(passivity.ispassive(sys)) + + # different combinations of zero A,B,C,D are 0 + B *= 0 + C *= 0 + assert(passivity.ispassive(sys)) + + A *= 0 + B = numpy.array([[0], [1]]) + C = numpy.array([[-1, 2]]) + D = numpy.array([[1.5]]) + assert(passivity.ispassive(sys)) + + B *= 0 + C *= 0 + assert(passivity.ispassive(sys)) + + A *= 0 + assert(passivity.ispassive(sys)) From bb16be01ff70919971d990a3228b6ff96b9b8182 Mon Sep 17 00:00:00 2001 From: mark-yeatman Date: Tue, 14 Jun 2022 11:26:30 -0400 Subject: [PATCH 15/20] Parameterize unit tests. Catch edge case of A=0. --- control/passivity.py | 7 +++-- control/tests/passivity_test.py | 53 +++++++++++++-------------------- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/control/passivity.py b/control/passivity.py index b00cc6b65..b2cf5a09e 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -44,6 +44,9 @@ def ispassive(sys): [n, m] = D.shape D = D + np.nextafter(0, 1)*np.eye(n, m) + [n, _] = A.shape + A = A - np.nextafter(0, 1)*np.eye(n) + def make_LMI_matrix(P): V = np.vstack(( np.hstack((A.T @ P + P@A, P@B)), @@ -75,7 +78,7 @@ def make_LMI_matrix(P): # crunch feasibility solution cvx.solvers.options['show_progress'] = False sol = cvx.solvers.sdp(c, - Gs=[cvx.matrix(coefficents)], - hs=[cvx.matrix(constants)]) + Gs=[cvx.matrix(coefficents)], + hs=[cvx.matrix(constants)]) return (sol["x"] is not None) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 171d3c542..d413f6fa5 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -2,7 +2,7 @@ Author: Mark Yeatman Date: May 30, 2022 ''' - +import pytest import numpy from control import ss, passivity from control.tests.conftest import cvxoptonly @@ -25,39 +25,26 @@ def test_ispassive(): assert(not passivity.ispassive(sys)) - +A_d = numpy.array([[-2, 0], [0, 0]]) +A = numpy.array([[-3, 0], [0, -2]]) +B = numpy.array([[0], [1]]) +C = numpy.array([[-1, 2]]) +D = numpy.array([[1.5]]) @cvxoptonly -def test_ispassive_edge_cases(): - A = numpy.array([[0, 1], [-2, -2]]) - B = numpy.array([[0], [1]]) - C = numpy.array([[-1, 2]]) - D = numpy.array([[1.5]]) - - D *= 0 +@pytest.mark.parametrize( + "test_input,expected", + [((A,B,C,D*0.0), True), + ((A_d,B,C,D), True), + ((A*1e12,B,C,D*0), True), + ((A,B*0,C*0,D), True), + ((A*0,B,C,D), True), + ((A*0,B*0,C*0,D*0), True)]) +def test_ispassive_edge_cases(test_input, expected): # strictly proper + A = test_input[0] + B = test_input[1] + C = test_input[2] + D = test_input[3] sys = ss(A, B, C, D) - assert(passivity.ispassive(sys)) - - # ill conditioned - A = A*1e12 - sys = ss(A, B, C, D) - assert(passivity.ispassive(sys)) - - # different combinations of zero A,B,C,D are 0 - B *= 0 - C *= 0 - assert(passivity.ispassive(sys)) - - A *= 0 - B = numpy.array([[0], [1]]) - C = numpy.array([[-1, 2]]) - D = numpy.array([[1.5]]) - assert(passivity.ispassive(sys)) - - B *= 0 - C *= 0 - assert(passivity.ispassive(sys)) - - A *= 0 - assert(passivity.ispassive(sys)) + assert(passivity.ispassive(sys)==expected) From cf0eac302a71897424ba8eeffd6b591cfbe1cde0 Mon Sep 17 00:00:00 2001 From: mark-yeatman Date: Tue, 14 Jun 2022 11:35:13 -0400 Subject: [PATCH 16/20] Run autopep8. --- control/passivity.py | 4 ++-- control/tests/passivity_test.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/control/passivity.py b/control/passivity.py index b2cf5a09e..60b081826 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -78,7 +78,7 @@ def make_LMI_matrix(P): # crunch feasibility solution cvx.solvers.options['show_progress'] = False sol = cvx.solvers.sdp(c, - Gs=[cvx.matrix(coefficents)], - hs=[cvx.matrix(constants)]) + Gs=[cvx.matrix(coefficents)], + hs=[cvx.matrix(constants)]) return (sol["x"] is not None) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index d413f6fa5..681d2f527 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -25,20 +25,23 @@ def test_ispassive(): assert(not passivity.ispassive(sys)) + A_d = numpy.array([[-2, 0], [0, 0]]) A = numpy.array([[-3, 0], [0, -2]]) B = numpy.array([[0], [1]]) C = numpy.array([[-1, 2]]) D = numpy.array([[1.5]]) + + @cvxoptonly @pytest.mark.parametrize( - "test_input,expected", - [((A,B,C,D*0.0), True), - ((A_d,B,C,D), True), - ((A*1e12,B,C,D*0), True), - ((A,B*0,C*0,D), True), - ((A*0,B,C,D), True), - ((A*0,B*0,C*0,D*0), True)]) + "test_input,expected", + [((A, B, C, D*0.0), True), + ((A_d, B, C, D), True), + ((A*1e12, B, C, D*0), True), + ((A, B*0, C*0, D), True), + ((A*0, B, C, D), True), + ((A*0, B*0, C*0, D*0), True)]) def test_ispassive_edge_cases(test_input, expected): # strictly proper @@ -47,4 +50,4 @@ def test_ispassive_edge_cases(test_input, expected): C = test_input[2] D = test_input[3] sys = ss(A, B, C, D) - assert(passivity.ispassive(sys)==expected) + assert(passivity.ispassive(sys) == expected) From 7e47d8060c49dd89b52fc62e849da7c521a51119 Mon Sep 17 00:00:00 2001 From: mark-yeatman Date: Wed, 15 Jun 2022 21:22:54 -0400 Subject: [PATCH 17/20] Add wrapper like functionality for ispassive(), so that it can be called in an object oriented style as a LTI class member. Added unit tests for transfer function and oo style calls. Ran autopep8 on lti.py. --- control/lti.py | 18 +++++++++++++----- control/passivity.py | 3 +++ control/tests/passivity_test.py | 22 +++++++++++++++++++++- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/control/lti.py b/control/lti.py index fdb4946cd..4f6624748 100644 --- a/control/lti.py +++ b/control/lti.py @@ -13,6 +13,7 @@ """ import numpy as np + from numpy import absolute, real, angle, abs from warnings import warn from . import config @@ -21,6 +22,7 @@ __all__ = ['poles', 'zeros', 'damp', 'evalfr', 'frequency_response', 'freqresp', 'dcgain', 'pole', 'zero'] + class LTI(NamedIOSystem): """LTI is a parent class to linear time-invariant (LTI) system objects. @@ -44,6 +46,7 @@ class LTI(NamedIOSystem): Note: dt processing has been moved to the NamedIOSystem class. """ + def __init__(self, inputs=1, outputs=1, states=None, name=None, **kwargs): """Assign the LTI object's numbers of inputs and ouputs.""" super().__init__( @@ -71,8 +74,7 @@ def _set_inputs(self, value): #: Deprecated inputs = property( - _get_inputs, _set_inputs, doc= - """ + _get_inputs, _set_inputs, doc=""" Deprecated attribute; use :attr:`ninputs` instead. The ``inputs`` attribute was used to store the number of system @@ -94,8 +96,7 @@ def _set_outputs(self, value): #: Deprecated outputs = property( - _get_outputs, _set_outputs, doc= - """ + _get_outputs, _set_outputs, doc=""" Deprecated attribute; use :attr:`noutputs` instead. The ``outputs`` attribute was used to store the number of system @@ -201,6 +202,11 @@ def _dcgain(self, warn_infinite): else: return zeroresp + def ispassive(self): + # importing here prevents circular dependancy + from control.passivity import ispassive + ispassive(self) + # # Deprecated functions # @@ -321,7 +327,7 @@ def damp(sys, doprint=True): wn, damping, poles = sys.damp() if doprint: print('_____Eigenvalue______ Damping___ Frequency_') - for p, d, w in zip(poles, damping, wn) : + for p, d, w in zip(poles, damping, wn): if abs(p.imag) < 1e-12: print("%10.4g %10.4g %10.4g" % (p.real, 1.0, -p.real)) @@ -330,6 +336,7 @@ def damp(sys, doprint=True): (p.real, p.imag, d, w)) return wn, damping, poles + def evalfr(sys, x, squeeze=None): """Evaluate the transfer function of an LTI system for complex frequency x. @@ -388,6 +395,7 @@ def evalfr(sys, x, squeeze=None): """ return sys.__call__(x, squeeze=squeeze) + def frequency_response(sys, omega, squeeze=None): """Frequency response of an LTI system at multiple angular frequencies. diff --git a/control/passivity.py b/control/passivity.py index 60b081826..e833fbf96 100644 --- a/control/passivity.py +++ b/control/passivity.py @@ -4,6 +4,7 @@ ''' import numpy as np +from control import statesp as ss try: import cvxopt as cvx @@ -35,6 +36,8 @@ def ispassive(sys): if cvx is None: raise ModuleNotFoundError("cvxopt required for passivity module") + sys = ss._convert_to_statespace(sys) + A = sys.A B = sys.B C = sys.C diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 681d2f527..09ef42b4a 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -4,7 +4,7 @@ ''' import pytest import numpy -from control import ss, passivity +from control import ss, passivity, tf from control.tests.conftest import cvxoptonly @@ -51,3 +51,23 @@ def test_ispassive_edge_cases(test_input, expected): D = test_input[3] sys = ss(A, B, C, D) assert(passivity.ispassive(sys) == expected) + + +def test_transfer_function(): + sys = tf([1], [1, -2]) + assert(passivity.ispassive(sys)) + + sys = tf([1], [1, 2]) + assert(not passivity.ispassive(sys)) + + +def test_oo_style(): + A = numpy.array([[0, 1], [-2, -2]]) + B = numpy.array([[0], [1]]) + C = numpy.array([[-1, 2]]) + D = numpy.array([[1.5]]) + sys = ss(A, B, C, D) + assert(sys.ispassive()) + + sys = tf([1], [1, -2]) + assert(sys.ispassive()) From ce11d0be328af73179307d4b63071968175fd537 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Thu, 16 Jun 2022 13:28:32 +0200 Subject: [PATCH 18/20] mark the whole passivity_test module as skippable --- control/tests/passivity_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 09ef42b4a..2a12f10df 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -8,7 +8,9 @@ from control.tests.conftest import cvxoptonly -@cvxoptonly +pytestmark = cvxoptonly + + def test_ispassive(): A = numpy.array([[0, 1], [-2, -2]]) B = numpy.array([[0], [1]]) @@ -33,7 +35,6 @@ def test_ispassive(): D = numpy.array([[1.5]]) -@cvxoptonly @pytest.mark.parametrize( "test_input,expected", [((A, B, C, D*0.0), True), From f7d74b2a52e4653d602512664f6ac00111f897c6 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 16 Jun 2022 09:14:24 -0500 Subject: [PATCH 19/20] Fix bug in tests and lti.py. --- control/lti.py | 2 +- control/tests/passivity_test.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/control/lti.py b/control/lti.py index 4f6624748..b87944cd0 100644 --- a/control/lti.py +++ b/control/lti.py @@ -205,7 +205,7 @@ def _dcgain(self, warn_infinite): def ispassive(self): # importing here prevents circular dependancy from control.passivity import ispassive - ispassive(self) + return ispassive(self) # # Deprecated functions diff --git a/control/tests/passivity_test.py b/control/tests/passivity_test.py index 2a12f10df..791d70b6c 100644 --- a/control/tests/passivity_test.py +++ b/control/tests/passivity_test.py @@ -55,10 +55,10 @@ def test_ispassive_edge_cases(test_input, expected): def test_transfer_function(): - sys = tf([1], [1, -2]) + sys = tf([1], [1, 2]) assert(passivity.ispassive(sys)) - sys = tf([1], [1, 2]) + sys = tf([1], [1, -2]) assert(not passivity.ispassive(sys)) @@ -70,5 +70,5 @@ def test_oo_style(): sys = ss(A, B, C, D) assert(sys.ispassive()) - sys = tf([1], [1, -2]) + sys = tf([1], [1, 2]) assert(sys.ispassive()) From c2255b0b087696e09717cc1591c1038b357f4ae7 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 16 Jun 2022 09:45:50 -0500 Subject: [PATCH 20/20] Fix merge issue. --- control/tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index ac35748f3..1201b8746 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -17,6 +17,8 @@ # pytest.param(marks=) slycotonly = pytest.mark.skipif(not control.exception.slycot_check(), reason="slycot not installed") +cvxoptonly = pytest.mark.skipif(not control.exception.cvxopt_check(), + reason="cvxopt not installed") matrixfilter = pytest.mark.filterwarnings("ignore:.*matrix subclass:" "PendingDeprecationWarning") matrixerrorfilter = pytest.mark.filterwarnings("error:.*matrix subclass:"