Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,17 @@

# Define module default parameter values
_statesp_defaults = {
'statesp.use_numpy_matrix': True,
'statesp.use_numpy_matrix': False,

@bnavigator bnavigator Jul 23, 2020 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we only change defaults in a major release?

How about keeping the default, but adding a CI job as proposed to maintain ndarray compatibility until the default is changed for good? At the time of the default change, keep a CI job that tests for compatibility with matrix as long as it is supported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed. I don't know what the release plans are for big changes (wasn't sure this would go into the next release), but let me push a commit that reverses this for now, but that way we can still have unit test fixes.

'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
----------
Expand All @@ -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
Expand Down Expand Up @@ -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]))
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion control/tests/discrete_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions control/tests/iosys_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand 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]'),
Expand Down
9 changes: 7 additions & 2 deletions control/tests/statesp_array_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 2 additions & 2 deletions control/tests/statesp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
50 changes: 12 additions & 38 deletions control/tests/test_control_matlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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')

Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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')
Expand All @@ -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)

Expand Down