8000 Merge pull request #433 from sawyerbfuller/ndarray-is-default · python-control/python-control@07a7c7a · GitHub
[go: up one dir, main page]

Skip to content

Commit 07a7c7a

Browse files
authored
Merge pull request #433 from sawyerbfuller/ndarray-is-default
fixes to unit tests so that they pass when the default array type is ndarray
2 parents c6a000f + ebd73c1 commit 07a7c7a

File tree

10 files changed

+90
-84
lines changed

10 files changed

+90
-84
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ jobs:
4949
services: xvfb
5050
python: "3.8"
5151
env: SCIPY=scipy SLYCOT=source
52+
- name: "use numpy matrix"
53+
dist: xenial
54+
services: xvfb
55+
python: "3.8"
56+
env: SCIPY=scipy SLYCOT=source PYTHON_CONTROL_STATESPACE_ARRAY=1
5257

5358
# Exclude combinations that are very unlikely (and don't work)
5459
exclude:

control/canonical.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from .statesp import StateSpace
77
from .statefbk import ctrb, obsv
88

9-
from numpy import zeros, shape, poly, iscomplex, hstack, dot, transpose
9+
from numpy import zeros, zeros_like, shape, poly, iscomplex, vstack, hstack, dot, \
10+
transpose, empty
1011
from numpy.linalg import solve, matrix_rank, eig
1112

1213
__all__ = ['canonical_form', 'reachable_form', 'observable_form', 'modal_form',
@@ -70,9 +71,9 @@ def reachable_form(xsys):
7071
zsys = StateSpace(xsys)
7172

7273
# Generate the system matrices for the desired canonical form
73-
zsys.B = zeros(shape(xsys.B))
74+
zsys.B = zeros_like(xsys.B)
7475
zsys.B[0, 0] = 1.0
75-
zsys.A = zeros(shape(xsys.A))
76+
zsys.A = zeros_like(xsys.A)
7677
Apoly = poly(xsys.A) # characteristic polynomial
7778
for i in range(0, xsys.states):
7879
zsys.A[0, i] = -Apoly[i+1] / Apoly[0]
@@ -124,9 +125,9 @@ def observable_form(xsys):
124125
zsys = StateSpace(xsys)
125126

126127
# Generate the system matrices for the desired canonical form
127-
zsys.C = zeros(shape(xsys.C))
128+
zsys.C = zeros_like(xsys.C)
128129
zsys.C[0, 0] = 1
129-
zsys.A = zeros(shape(xsys.A))
130+
zsys.A = zeros_like(xsys.A)
130131
Apoly = poly(xsys.A) # characteristic polynomial
131132
for i in range(0, xsys.states):
132133
zsys.A[i, 0] = -Apoly[i+1] / Apoly[0]
@@ -144,7 +145,7 @@ def observable_form(xsys):
144145
raise ValueError("Transformation matrix singular to working precision.")
145146

146147
# Finally, compute the output matrix
147-
zsys.B = Tzx * xsys.B
148+
zsys.B = Tzx.dot(xsys.B)
148149

149150
return zsys, Tzx
150151

@@ -174,9 +175,9 @@ def modal_form(xsys):
174175
# Calculate eigenvalues and matrix of eigenvectors Tzx,
175176
eigval, eigvec = eig(xsys.A)
176177

177-
# Eigenvalues and according eigenvectors are not sorted,
178+
# Eigenvalues and corresponding eigenvectors are not sorted,
178179
# thus modal transformation is ambiguous
179-
# Sorting eigenvalues and respective vectors by largest to smallest eigenvalue
180+
# Sort eigenvalues and vectors from largest to smallest eigenvalue
180181
idx = eigval.argsort()[::-1]
181182
eigval = eigval[idx]
182183
eigvec = eigvec[:,idx]
@@ -189,23 +190,18 @@ def modal_form(xsys):
189190

190191
# Keep track of complex conjugates (need only one)
191192
lst_conjugates = []
192-
Tzx = None
193+
Tzx = empty((0, xsys.A.shape[0])) # empty zero-height row matrix
193194
for val, vec in zip(eigval, eigvec.T):
194195
if iscomplex(val):
195196
if val not in lst_conjugates:
196197
lst_conjugates.append(val.conjugate())
197-
if Tzx is not None:
198-
Tzx 9E88 = hstack((Tzx, hstack((vec.real.T, vec.imag.T))))
199-
else:
200-
Tzx = hstack((vec.real.T, vec.imag.T))
198+
Tzx = vstack((Tzx, vec.real, vec.imag))
201199
else:
202200
# if conjugate has already been seen, skip this eigenvalue
203201
lst_conjugates.remove(val)
204202
else:
205-
if Tzx is not None:
206-
Tzx = hstack((Tzx, vec.real.T))
207-
else:
208-
Tzx = vec.real.T
203+
Tzx = vstack((Tzx, vec.real))
204+
Tzx = Tzx.T
209205

210206
# Generate the system matrices for the desired canonical form
211207
zsys.A = solve(Tzx, xsys.A).dot(Tzx)

control/statesp.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@
7777

7878

7979
def _ssmatrix(data, axis=1):
80-
"""Convert argument to a (possibly empty) state space matrix.
80+
"""Convert argument to a (possibly empty) 2D state space matrix.
81+
82+
The axis keyword argument makes it convenient to specify that if the input
83+
is a vector, it is a row (axis=1) or column (axis=0) vector.
8184
8285
Parameters
8386
----------
@@ -94,8 +97,10 @@ def _ssmatrix(data, axis=1):
9497
"""
9598
# Convert the data into an array or matrix, as configured
9699
# If data is passed as a string, use (deprecated?) matrix constructor
97-
if config.defaults['statesp.use_numpy_matrix'] or isinstance(data, str):
100+
if config.defaults['statesp.use_numpy_matrix']:
98101
arr = np.matrix(data, dtype=float)
102+
elif isinstance(data, str):
103+
arr = np.array(np.matrix(data, dtype=float))
99104
else:
100105
arr = np.array(data, dtype=float)
101106
ndim = arr.ndim
@@ -195,12 +200,20 @@ def __init__(self, *args, **kw):
195200
raise ValueError("Needs 1 or 4 arguments; received %i." % len(args))
196201

197202
# Process keyword arguments
198-
remove_useless = kw.get('remove_useless', config.defaults['statesp.remove_useless_states'])
203+
remove_useless = kw.get('remove_useless',
204+
config.defaults['statesp.remove_useless_states'])
199205

200206
# Convert all matrices to standard form
201207
A = _ssmatrix(A)
202-
B = _ssmatrix(B, axis=0)
203-
C = _ssmatrix(C, axis=1)
208+
# if B is a 1D array, turn it into a column vector if it fits
209+
if np.asarray(B).ndim == 1 and len(B) == A.shape[0]:
210+
B = _ssmatrix(B, axis=0)
211+
else:
212+
B = _ssmatrix(B)
213+
if np.asarray(C).ndim == 1 and len(C) == A.shape[0]:
214+
C = _ssmatrix(C, axis=1)
215+
else:
216+
C = _ssmatrix(C, axis=0) #if this doesn't work, error below
204217
if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0:
205218
# If D is a scalar zero, broadcast it to the proper size
206219
D = np.zeros((C.shape[0], B.shape[1]))
@@ -1240,8 +1253,8 @@ def _mimo2simo(sys, input, warn_conversion=False):
12401253
"Only input {i} is used." .format(i=input))
12411254
# $X = A*X + B*U
12421255
# Y = C*X + D*U
1243-
new_B = sys.B[:, input]
1244-
new_D = sys.D[:, input]
1256+
new_B = sys.B[:, input:input+1]
1257+
new_D = sys.D[:, input:input+1]
12451258
sys = StateSpace(sys.A, new_B, sys.C, new_D, sys.dt)
12461259

12471260
return sys

control/tests/canonical_test.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ def test_reachable_form(self):
2222
D_true = 42.0
2323

2424
# Perform a coordinate transform with a random invertible matrix
25-
T_true = np.matrix([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
25+
T_true = np.array([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
2626
[-0.74855725, -0.39136285, -0.18142339, -0.50356997],
2727
[-0.40688007, 0.81416369, 0.38002113, -0.16483334],
2828
[-0.44769516, 0.15654653, -0.50060858, 0.72419146]])
29-
A = np.linalg.solve(T_true, A_true)*T_true
29+
A = np.linalg.solve(T_true, A_true).dot(T_true)
3030
B = np.linalg.solve(T_true, B_true)
31-
C = C_true*T_true
31+
C = C_true.dot(T_true)
3232
D = D_true
3333

3434
# Create a state space system and convert it to the reachable canonical form
@@ -69,11 +69,11 @@ def test_modal_form(self):
6969
D_true = 42.0
7070

7171
# Perform a coordinate transform with a random invertible matrix
72-
T_true = np.matrix([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
72+
T_true = np.array([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
7373
[-0.74855725, -0.39136285, -0.18142339, -0.50356997],
7474
[-0.40688007, 0.81416369, 0.38002113, -0.16483334],
7575
[-0.44769516, 0.15654653, -0.50060858, 0.72419146]])
76-
A = np.linalg.solve(T_true, A_true)*T_true
76+
A = np.linalg.solve(T_true, A_true).dot(T_true)
7777
B = np.linalg.solve(T_true, B_true)
7878
C = C_true*T_true
7979
D = D_true
@@ -98,9 +98,9 @@ def test_modal_form(self):
9898
C_true = np.array([[1, 0, 0, 1]])
9999
D_true = np.array([[0]])
100100

101-
A = np.linalg.solve(T_true, A_true) * T_true
101+
A = np.linalg.solve(T_true, A_true).dot(T_true)
102102
B = np.linalg.solve(T_true, B_true)
103-
C = C_true * T_true
103+
C = C_true.dot(T_true)
104104
D = D_true
105105

106106
# Create state space system and convert to modal canonical form
@@ -132,9 +132,9 @@ def test_modal_form(self):
132132
C_true = np.array([[0, 1, 0, 1]])
133133
D_true = np.array([[0]])
134134

135-
A = np.linalg.solve(T_true, A_true) * T_true
135+
A = np.linalg.solve(T_true, A_true).dot(T_true)
136136
B = np.linalg.solve(T_true, B_true)
137-
C = C_true * T_true
137+
C = C_true.dot(T_true)
138138
D = D_true
139139

140140
# Create state space system and convert to modal canonical form
@@ -173,13 +173,13 @@ def test_observable_form(self):
173173
D_true = 42.0
174174

175175
# Perform a coordinate transform with a random invertible matrix
176-
T_true = np.matrix([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
176+
T_true = np.array([[-0.27144004, -0.39933167, 0.75634684, 0.44135471],
177177
[-0.74855725, -0.39136285, -0.18142339, -0.50356997],
178178
[-0.40688007, 0.81416369, 0.38002113, -0.16483334],
179179
[-0.44769516, 0.15654653, -0.50060858, 0.72419146]])
180-
A = np.linalg.solve(T_true, A_true)*T_true
180+
A = np.linalg.solve(T_true, A_true).dot(T_true)
181181
B = np.linalg.solve(T_true, B_true)
182-
C = C_true*T_true
182+
C = C_true.dot(T_true)
183183
D = D_true
184184

185185
# Create a state space system and convert it to the observable canonical form

control/tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# contest.py - pytest local plugins and fixtures
2+
3+
import control
4+
import os
5+
6+
import pytest
7+
8+
9+
@pytest.fixture(scope="session", autouse=True)
10+
def use_numpy_ndarray():
11+
"""Switch the config to use ndarray instead of matrix"""
12+
if os.getenv("PYTHON_CONTROL_STATESPACE_ARRAY") == "1":
13+
control.config.defaults['statesp.use_numpy_matrix'] = False

control/tests/discrete_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def test_sample_ss(self):
353353
for sys in (sys1, sys2):
354354
for h in (0.1, 0.5, 1, 2):
355355
Ad = I + h * sys.A
356-
Bd = h * sys.B + 0.5 * h**2 * (sys.A * sys.B)
356+
Bd = h * sys.B + 0.5 * h**2 * np.dot(sys.A, sys.B)
357357
sysd = sample_system(sys, h, method='zoh')
358358
np.testing.assert_array_almost_equal(sysd.A, Ad)
359359
np.testing.assert_array_almost_equal(sysd.B, Bd)

control/tests/iosys_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_linear_iosys(self):
5353
for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)):
5454
np.testing.assert_array_almost_equal(
5555
np.reshape(iosys._rhs(0, x, u), (-1,1)),
56-
linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u)
56+
np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u))
5757

5858
# Make sure that simulations also line up
5959
T, U, X0 = self.T, self.U, self.X0
@@ -151,9 +151,9 @@ def test_nonlinear_iosys(self):
151151

152152
# Create a nonlinear system with the same dynamics
153153
nlupd = lambda t, x, u, params: \
154-
np.reshape(linsys.A * np.reshape(x, (-1, 1)) + linsys.B * u, (-1,))
154+
np.reshape(np.dot(linsys.A, np.reshape(x, (-1, 1))) + np.dot(linsys.B, u), (-1,))
155155
nlout = lambda t, x, u, params: \
156-
np.reshape(linsys.C * np.reshape(x, (-1, 1)) + linsys.D * u, (-1,))
156+
np.reshape(np.dot(linsys.C, np.reshape(x, (-1, 1))) + np.dot(linsys.D, u), (-1,))
157157
nlsys = ios.NonlinearIOSystem(nlupd, nlout)
158158

159159
# Make sure that simulations also line up
@@ -747,8 +747,8 @@ def test_named_signals(self):
747747
+ np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1)))
748748
).reshape(-1,),
749749
outfcn = lambda t, x, u, params: np.array(
750-
self.mimo_linsys1.C * np.reshape(x, (-1, 1)) \
751-
+ self.mimo_linsys1.D * np.reshape(u, (-1, 1))
750+
np.dot(self.mimo_linsys1.C, np.reshape(x, (-1, 1))) \
751+
+ np.dot(self.mimo_linsys1.D, np.reshape(u, (-1, 1)))
752752
).reshape(-1,),
753753
inputs = ('u[0]', 'u[1]'),
754754
outputs = ('y[0]', 'y[1]'),

control/tests/statesp_array_test.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from control.lti import evalfr
1414
from control.exception import slycot_check
1515
from control.config import use_numpy_matrix, reset_defaults
16+
from control.config import defaults
1617

1718
class TestStateSpace(unittest.TestCase):
1819
"""Tests for the StateSpace class."""
@@ -74,8 +75,12 @@ def test_matlab_style_constructor(self):
7475
self.assertEqual(sys.B.shape, (2, 1))
7576
self.assertEqual(sys.C.shape, (1, 2))
7677
self.assertEqual(sys.D.shape, (1, 1))
77-
for X in [sys.A, sys.B, sys.C, sys.D]:
78-
self.assertTrue(isinstance(X, np.matrix))
78+
if defaults['statesp.use_numpy_matrix']:
79+
for X in [sys.A, sys.B, sys.C, sys.D]:
80+
self.assertTrue(isinstance(X, np.matrix))
81+
else:
82+
for X in [sys.A, sys.B, sys.C, sys.D]:
83+
self.assertTrue(isinstance(X, np.ndarray))
7984

8085
def test_pole(self):
8186
"""Evaluate the poles of a MIMO system."""

control/tests/statesp_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ def test_array_access_ss(self):
323323
np.testing.assert_array_almost_equal(sys1_11.A,
324324
sys1.A)
325325
np.testing.assert_array_almost_equal(sys1_11.B,
326-
sys1.B[:, 1])
326+
sys1.B[:, 1:2])
327327
np.testing.assert_array_almost_equal(sys1_11.C,
328-
sys1.C[0, :])
328+
sys1.C[0:1, :])
329329
np.testing.assert_array_almost_equal(sys1_11.D,
330330
sys1.D[0, 1])
331331

0 commit comments

Comments
 (0)
0