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:" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy