From 7e7ae9c6819c3fa26a1cc1b61a58d094eb46c416 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 22 Jul 2020 12:55:15 -0700 Subject: [PATCH 1/5] initial commit changing default array type in statespace to be ndarray instead of matrix. two failing unit tests to fix still --- control/statesp.py | 29 +++++++++++----- control/tests/discrete_test.py | 2 +- control/tests/iosys_test.py | 2 +- control/tests/statesp_array_test.py | 9 +++-- control/tests/statesp_test.py | 4 +-- control/tests/test_control_matlab.py | 50 +++++++--------------------- 6 files changed, 44 insertions(+), 52 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 522d187a9..4f8704d45 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -70,14 +70,17 @@ # Define module default parameter values _statesp_defaults = { - 'statesp.use_numpy_matrix': True, + 'statesp.use_numpy_matrix': False, 'statesp.default_dt': None, 'statesp.remove_useless_states': True, } def _ssmatrix(data, axis=1): - """Convert argument to a (possibly empty) state space matrix. + """Convert argument to a (possibly empty) 2D state space matrix. + + The axis keyword argument makes it convenient to specify that if the input + is a vector, it is a row (axis=1) or column (axis=0) vector. Parameters ---------- @@ -94,8 +97,10 @@ def _ssmatrix(data, axis=1): """ # Convert the data into an array or matrix, as configured # If data is passed as a string, use (deprecated?) matrix constructor - if config.defaults['statesp.use_numpy_matrix'] or isinstance(data, str): + if config.defaults['statesp.use_numpy_matrix']: arr = np.matrix(data, dtype=float) + elif isinstance(data, str): + arr = np.array(np.matrix(data, dtype=float)) else: arr = np.array(data, dtype=float) ndim = arr.ndim @@ -195,12 +200,20 @@ def __init__(self, *args, **kw): raise ValueError("Needs 1 or 4 arguments; received %i." % len(args)) # Process keyword arguments - remove_useless = kw.get('remove_useless', config.defaults['statesp.remove_useless_states']) + remove_useless = kw.get('remove_useless', + config.defaults['statesp.remove_useless_states']) # Convert all matrices to standard form A = _ssmatrix(A) - B = _ssmatrix(B, axis=0) - C = _ssmatrix(C, axis=1) + # if B is a 1D array, turn it into a column vector if it fits + if np.asarray(B).ndim == 1 and len(B) == A.shape[0]: + B = _ssmatrix(B, axis=0) + else: + B = _ssmatrix(B) + if np.asarray(C).ndim == 1 and len(C) == A.shape[0]: + C = _ssmatrix(C, axis=1) + else: + C = _ssmatrix(C, axis=0) #if this doesn't work, error below if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0: # If D is a scalar zero, broadcast it to the proper size D = np.zeros((C.shape[0], B.shape[1])) @@ -1240,8 +1253,8 @@ def _mimo2simo(sys, input, warn_conversion=False): "Only input {i} is used." .format(i=input)) # $X = A*X + B*U # Y = C*X + D*U - new_B = sys.B[:, input] - new_D = sys.D[:, input] + new_B = sys.B[:, input:input+1] + new_D = sys.D[:, input:input+1] sys = StateSpace(sys.A, new_B, sys.C, new_D, sys.dt) return sys diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 6598e3a81..9c1928dab 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -353,7 +353,7 @@ def test_sample_ss(self): for sys in (sys1, sys2): for h in (0.1, 0.5, 1, 2): Ad = I + h * sys.A - Bd = h * sys.B + 0.5 * h**2 * (sys.A * sys.B) + Bd = h * sys.B + 0.5 * h**2 * np.dot(sys.A, sys.B) sysd = sample_system(sys, h, method='zoh') np.testing.assert_array_almost_equal(sysd.A, Ad) np.testing.assert_array_almost_equal(sysd.B, Bd) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 0738e8b18..64f6d350e 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -53,7 +53,7 @@ def test_linear_iosys(self): for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)): np.testing.assert_array_almost_equal( np.reshape(iosys._rhs(0, x, u), (-1,1)), - linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u) + np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u)) # Make sure that simulations also line up T, U, X0 = self.T, self.U, self.X0 diff --git a/control/tests/statesp_array_test.py b/control/tests/statesp_array_test.py index a2d034075..f0574cf24 100644 --- a/control/tests/statesp_array_test.py +++ b/control/tests/statesp_array_test.py @@ -13,6 +13,7 @@ from control.lti import evalfr from control.exception import slycot_check from control.config import use_numpy_matrix, reset_defaults +from control.config import defaults class TestStateSpace(unittest.TestCase): """Tests for the StateSpace class.""" @@ -74,8 +75,12 @@ def test_matlab_style_constructor(self): self.assertEqual(sys.B.shape, (2, 1)) self.assertEqual(sys.C.shape, (1, 2)) self.assertEqual(sys.D.shape, (1, 1)) - for X in [sys.A, sys.B, sys.C, sys.D]: - self.assertTrue(isinstance(X, np.matrix)) + if defaults['statesp.use_numpy_matrix']: + for X in [sys.A, sys.B, sys.C, sys.D]: + self.assertTrue(isinstance(X, np.matrix)) + else: + for X in [sys.A, sys.B, sys.C, sys.D]: + self.assertTrue(isinstance(X, np.ndarray)) def test_pole(self): """Evaluate the poles of a MIMO system.""" diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 96404d79f..34a17f992 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -323,9 +323,9 @@ def test_array_access_ss(self): np.testing.assert_array_almost_equal(sys1_11.A, sys1.A) np.testing.assert_array_almost_equal(sys1_11.B, - sys1.B[:, 1]) + sys1.B[:, 1:2]) np.testing.assert_array_almost_equal(sys1_11.C, - sys1.C[0, :]) + sys1.C[0:1, :]) np.testing.assert_array_almost_equal(sys1_11.D, sys1.D[0, 1]) diff --git a/control/tests/test_control_matlab.py b/control/tests/test_control_matlab.py index e45b52523..aa8633e7c 100644 --- a/control/tests/test_control_matlab.py +++ b/control/tests/test_control_matlab.py @@ -11,7 +11,7 @@ from numpy.testing import assert_array_almost_equal from numpy import array, asarray, matrix, asmatrix, zeros, ones, linspace,\ all, hstack, vstack, c_, r_ -from matplotlib.pylab import show, figure, plot, legend, subplot2grid +from matplotlib.pyplot import show, figure, plot, legend, subplot2grid from control.matlab import ss, step, impulse, initial, lsim, dcgain, \ ss2tf from control.statesp import _mimo2siso @@ -24,29 +24,13 @@ class TestControlMatlab(unittest.TestCase): def setUp(self): pass - def plot_matrix(self): - #Test: can matplotlib correctly plot matrices? - #Yes, but slightly inconvenient - figure() - t = matrix([[ 1.], - [ 2.], - [ 3.], - [ 4.]]) - y = matrix([[ 1., 4.], - [ 4., 5.], - [ 9., 6.], - [16., 7.]]) - plot(t, y) - #plot(asarray(t)[0], asarray(y)[0]) - - def make_SISO_mats(self): """Return matrices for a SISO system""" - A = matrix([[-81.82, -45.45], + A = array([[-81.82, -45.45], [ 10., -1. ]]) - B = matrix([[9.09], + B = array([[9.09], [0. ]]) - C = matrix([[0, 0.159]]) + C = array([[0, 0.159]]) D = zeros((1, 1)) return A, B, C, D @@ -181,7 +165,7 @@ def test_impulse(self): #Test MIMO system A, B, C, D = self.make_MIMO_mats() - sys = ss(A, B, C, D) + sys = ss(A, B, C, D) t, y = impulse(sys) plot(t, y, label='MIMO System') @@ -202,7 +186,7 @@ def test_initial(self): #X0=[1,1] : produces a spike subplot2grid(plot_shape, (0, 1)) - t, y = initial(sys, X0=matrix("1; 1")) + t, y = initial(sys, X0=array(matrix("1; 1"))) plot(t, y) #Test MIMO system @@ -318,21 +302,11 @@ def test_lsim(self): plot(t, y, label='y') legend(loc='best') - #Test with matrices - subplot2grid(plot_shape, (1, 0)) - t = matrix(linspace(0, 1, 100)) - u = matrix(r_[1:1:50j, 0:0:50j]) - x0 = matrix("0.; 0") - y, t_out, _x = lsim(sys, u, t, x0) - plot(t_out, y, label='y') - plot(t_out, asarray(u/10)[0], label='u/10') - legend(loc='best') - #Test with MIMO system subplot2grid(plot_shape, (1, 1)) A, B, C, D = self.make_MIMO_mats() sys = ss(A, B, C, D) - t = matrix(linspace(0, 1, 100)) + t = array(linspace(0, 1, 100)) u = array([r_[1:1:50j, 0:0:50j], r_[0:1:50j, 0:0:50j]]) x0 = [0, 0, 0, 0] @@ -404,12 +378,12 @@ def test_convert_MIMO_to_SISO(self): #Test with additional systems -------------------------------------------- #They have crossed inputs and direct feedthrough #SISO system - As = matrix([[-81.82, -45.45], + As = array([[-81.82, -45.45], [ 10., -1. ]]) - Bs = matrix([[9.09], + Bs = array([[9.09], [0. ]]) - Cs = matrix([[0, 0.159]]) - Ds = matrix([[0.02]]) + Cs = array([[0, 0.159]]) + Ds = array([[0.02]]) sys_siso = ss(As, Bs, Cs, Ds) # t, y = step(sys_siso) # plot(t, y, label='sys_siso d=0.02') @@ -428,7 +402,7 @@ def test_convert_MIMO_to_SISO(self): [0 , 0 ]]) Cm = array([[0, 0, 0, 0.159], [0, 0.159, 0, 0 ]]) - Dm = matrix([[0, 0.02], + Dm = array([[0, 0.02], [0.02, 0 ]]) sys_mimo = ss(Am, Bm, Cm, Dm) From 5ecc543c2fe0fed93859153155d267fd6f3bdd0f Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 23 Jul 2020 02:10:10 -0700 Subject: [PATCH 2/5] found two remaining unit test bugs! (switch from * to np.dot) --- control/tests/iosys_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 64f6d350e..22f8307d2 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -151,9 +151,9 @@ def test_nonlinear_iosys(self): # Create a nonlinear system with the same dynamics nlupd = lambda t, x, u, params: \ - np.reshape(linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u, (-1,)) + np.reshape(np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u), (-1,)) nlout = lambda t, x, u, params: \ - np.reshape(linsys.C * np.reshape(x, (-1, 1)) + linsys.D * u, (-1,)) + np.reshape(np.dot(linsys.C, np.reshape(x, (-1, 1))) + np.dot(linsys.D, u), (-1,)) nlsys = ios.NonlinearIOSystem(nlupd, nlout) # Make sure that simulations also line up @@ -747,8 +747,8 @@ def test_named_signals(self): + np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1))) ).reshape(-1,), outfcn = lambda t, x, u, params: np.array( - self.mimo_linsys1.C * np.reshape(x, (-1, 1)) \ - + self.mimo_linsys1.D * np.reshape(u, (-1, 1)) + np.dot(self.mimo_linsys1.C, np.reshape(x, (-1, 1))) \ + + np.dot(self.mimo_linsys1.D, np.reshape(u, (-1, 1))) ).reshape(-1,), inputs = ('u[0]', 'u[1]'), outputs = ('y[0]', 'y[1]'), From 606fa3fed10bab489d60720c04c7f1da25aa245d Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 23 Jul 2020 15:02:18 -0700 Subject: [PATCH 3/5] stick with old default for now --- control/statesp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/statesp.py b/control/statesp.py index 4f8704d45..526fc129b 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -70,7 +70,7 @@ # Define module default parameter values _statesp_defaults = { - 'statesp.use_numpy_matrix': False, + 'statesp.use_numpy_matrix': True, 'statesp.default_dt': None, 'statesp.remove_useless_states': True, } From eda91ca9b203b7663e6f07ebe6845447fe9cbd5e Mon Sep 17 00:00:00 2001 From: bnavigator Date: Fri, 24 Jul 2020 00:53:04 +0200 Subject: [PATCH 4/5] add conftest.py fixture and travis job to check with ndarray --- .travis.yml | 5 +++++ control/tests/conftest.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100755 control/tests/conftest.py diff --git a/.travis.yml b/.travis.yml index 022e48c6f..ec615501d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,6 +49,11 @@ jobs: services: xvfb python: "3.8" env: SCIPY=scipy SLYCOT=source + - name: "use numpy matrix" + dist: xenial + services: xvfb + python: "3.8" + env: SCIPY=scipy SLYCOT=source PYTHON_CONTROL_STATESPACE_ARRAY=1 # Exclude combinations that are very unlikely (and don't work) exclude: diff --git a/control/tests/conftest.py b/control/tests/conftest.py new file mode 100755 index 000000000..e98bbe1d7 --- /dev/null +++ b/control/tests/conftest.py @@ -0,0 +1,13 @@ +# contest.py - pytest local plugins and fixtures + +import control +import os + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def use_numpy_ndarray(): + """Switch the config to use ndarray instead of matrix""" + if os.getenv("PYTHON_CONTROL_STATESPACE_ARRAY") == "1": + control.config.defaults['statesp.use_numpy_matrix'] = False From ebd73c1d22efd121dc908498478bf9a94d0ca92d Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 24 Jul 2020 10:43:38 -0700 Subject: [PATCH 5/5] fixes in canonical and canonical_test that were assuming numpy.matrix --- control/canonical.py | 30 +++++++++++++----------------- control/tests/canonical_test.py | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/control/canonical.py b/control/canonical.py index b578418bd..bd9ee4a94 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -6,7 +6,8 @@ from .statesp import StateSpace from .statefbk import ctrb, obsv -from numpy import zeros, shape, poly, iscomplex, hstack, dot, transpose +from numpy import zeros, zeros_like, shape, poly, iscomplex, vstack, hstack, dot, \ + transpose, empty from numpy.linalg import solve, matrix_rank, eig __all__ = ['canonical_form', 'reachable_form', 'observable_form', 'modal_form', @@ -70,9 +71,9 @@ def reachable_form(xsys): zsys = StateSpace(xsys) # Generate the system matrices for the desired canonical form - zsys.B = zeros(shape(xsys.B)) + zsys.B = zeros_like(xsys.B) zsys.B[0, 0] = 1.0 - zsys.A = zeros(shape(xsys.A)) + zsys.A = zeros_like(xsys.A) Apoly = poly(xsys.A) # characteristic polynomial for i in range(0, xsys.states): zsys.A[0, i] = -Apoly[i+1] / Apoly[0] @@ -124,9 +125,9 @@ def observable_form(xsys): zsys = StateSpace(xsys) # Generate the system matrices for the desired canonical form - zsys.C = zeros(shape(xsys.C)) + zsys.C = zeros_like(xsys.C) zsys.C[0, 0] = 1 - zsys.A = zeros(shape(xsys.A)) + zsys.A = zeros_like(xsys.A) Apoly = poly(xsys.A) # characteristic polynomial for i in range(0, xsys.states): zsys.A[i, 0] = -Apoly[i+1] / Apoly[0] @@ -144,7 +145,7 @@ def observable_form(xsys): raise ValueError("Transformation matrix singular to working precision.") # Finally, compute the output matrix - zsys.B = Tzx * xsys.B + zsys.B = Tzx.dot(xsys.B) return zsys, Tzx @@ -174,9 +175,9 @@ def modal_form(xsys): # Calculate eigenvalues and matrix of eigenvectors Tzx, eigval, eigvec = eig(xsys.A) - # Eigenvalues and according eigenvectors are not sorted, + # Eigenvalues and corresponding eigenvectors are not sorted, # thus modal transformation is ambiguous - # Sorting eigenvalues and respective vectors by largest to smallest eigenvalue + # Sort eigenvalues and vectors from largest to smallest eigenvalue idx = eigval.argsort()[::-1] eigval = eigval[idx] eigvec = eigvec[:,idx] @@ -189,23 +190,18 @@ def modal_form(xsys): # Keep track of complex conjugates (need only one) lst_conjugates = [] - Tzx = None + Tzx = empty((0, xsys.A.shape[0])) # empty zero-height row matrix 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)) + Tzx = vstack((Tzx, vec.real, vec.imag)) 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 + Tzx = vstack((Tzx, vec.real)) + Tzx = Tzx.T # Generate the system matrices for the desired canonical form zsys.A = solve(Tzx, xsys.A).dot(Tzx) diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index 3172f13b7..7d4ae4e27 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -22,13 +22,13 @@ def test_reachable_form(self): 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], + T_true = np.array([[-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 + A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) - C = C_true*T_true + C = C_true.dot(T_true) D = D_true # Create a state space system and convert it to the reachable canonical form @@ -69,11 +69,11 @@ def test_modal_form(self): 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], + T_true = np.array([[-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 + A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) C = C_true*T_true D = D_true @@ -98,9 +98,9 @@ def test_modal_form(self): C_true = np.array([[1, 0, 0, 1]]) D_true = np.array([[0]]) - A = np.linalg.solve(T_true, A_true) * T_true + A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) - C = C_true * T_true + C = C_true.dot(T_true) D = D_true # Create state space system and convert to modal canonical form @@ -132,9 +132,9 @@ def test_modal_form(self): C_true = np.array([[0, 1, 0, 1]]) D_true = np.array([[0]]) - A = np.linalg.solve(T_true, A_true) * T_true + A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) - C = C_true * T_true + C = C_true.dot(T_true) D = D_true # Create state space system and convert to modal canonical form @@ -173,13 +173,13 @@ def test_observable_form(self): 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], + T_true = np.array([[-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 + A = np.linalg.solve(T_true, A_true).dot(T_true) B = np.linalg.solve(T_true, B_true) - C = C_true*T_true + C = C_true.dot(T_true) D = D_true # Create a state space system and convert it to the observable canonical form 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