diff --git a/.gitignore b/.gitignore index c63a6cf06..0262ab46f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __conda_*.txt record.txt build.log *.egg-info/ +.eggs/ .coverage doc/_build doc/generated @@ -18,3 +19,7 @@ examples/.ipynb_checkpoints/ .project Untitled*.ipynb *.idea/ + +# Files created by or for emacs (RMM, 29 Dec 2017) +*~ +TAGS diff --git a/control/tests/input_element_int_test.py b/control/tests/input_element_int_test.py new file mode 100644 index 000000000..c6a6f64a3 --- /dev/null +++ b/control/tests/input_element_int_test.py @@ -0,0 +1,54 @@ +# input_element_int_test.py +# +# Author: Kangwon Lee (kangwonlee) +# Date: 22 Oct 2017 +# +# Unit tests contributed as part of PR #158, "SISO tf() may not work +# with numpy arrays with numpy.int elements" +# +# Modified: +# * 29 Dec 2017, RMM - updated file name and added header + +import unittest +import numpy as np +import control as ctl + +class TestTfInputIntElement(unittest.TestCase): + # currently these do not pass + def test_tf_den_with_numpy_int_element(self): + num = 1 + den = np.convolve([1, 2, 1], [1, 1, 1]) + + sys = ctl.tf(num, den) + + self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + + def test_tf_num_with_numpy_int_element(self): + num = np.convolve([1], [1, 1]) + den = np.convolve([1, 2, 1], [1, 1, 1]) + + sys = ctl.tf(num, den) + + self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + + # currently these pass + def test_tf_input_with_int_element_works(self): + num = 1 + den = np.convolve([1.0, 2, 1], [1, 1, 1]) + + sys = ctl.tf(num, den) + + self.assertAlmostEqual(1.0, ctl.dcgain(sys)) + + def test_ss_input_with_int_element(self): + ident = np.matrix(np.identity(2), dtype=int) + a = np.matrix([[0, 1], + [-1, -2]], dtype=int) * ident + b = np.matrix([[0], + [1]], dtype=int) + c = np.matrix([[0, 1]], dtype=int) + d = 0 + + sys = ctl.ss(a, b, c, d) + sys2 = ctl.ss2tf(sys) + self.assertAlmostEqual(ctl.dcgain(sys), ctl.dcgain(sys2)) diff --git a/control/tests/xferfcn_input_test.py b/control/tests/xferfcn_input_test.py new file mode 100644 index 000000000..acddc64ae --- /dev/null +++ b/control/tests/xferfcn_input_test.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# +# xferfcn_input_test.py - test inputs to TransferFunction class +# jed-frey, 18 Feb 2017 (based on xferfcn_test.py) + +import unittest +import numpy as np + +from numpy import int, int8, int16, int32, int64 +from numpy import float, float16, float32, float64, float128 +from numpy import all, ndarray, array + +from control.xferfcn import _cleanPart + +class TestXferFcnInput(unittest.TestCase): + """These are tests for functionality of cleaning and validating + XferFucnInput.""" + + # Tests for raising exceptions. + def testBadInputType(self): + """Give the part cleaner invalid input type.""" + + self.assertRaises(TypeError, _cleanPart, [[0., 1.], [2., 3.]]) + + def testBadInputType2(self): + """Give the part cleaner another invalid input type.""" + self.assertRaises(TypeError, _cleanPart, [1,"a"]) + + def testScalar(self): + """Test single scalar value.""" + num = 1 + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testListScalar(self): + """Test single scalar value in list.""" + num = [1] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testTupleScalar(self): + """Test single scalar value in tuple.""" + num = (1) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testList(self): + """Test multiple values in a list.""" + num = [1, 2] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) + + def testTuple(self): + """Test multiple values in tuple.""" + num = (1, 2) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) + + def testAllScalarTypes(self): + """Test single scalar value for all valid data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = dtype(1) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testNpArray(self): + """Test multiple values in numpy array.""" + num = np.array([1, 2]) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) + + def testAllNumpyArrayTypes(self): + """Test scalar value in numpy array of ndim=0 for all data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = np.array(1, dtype=dtype) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testAllNumpyArrayTypes2(self): + """Test numpy array for all types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = np.array([1, 2], dtype=dtype) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) + + def testListAllTypes(self): + """Test list of a single value for all data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = [dtype(1)] + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testListAllTypes2(self): + """List of list of numbers of all data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = [dtype(1), dtype(2)] + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 2.0], dtype=float)) + + def testTupleAllTypes(self): + """Test tuple of a single value for all data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = (dtype(1),) + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testTupleAllTypes2(self): + """Test tuple of a single value for all data types.""" + for dtype in [int, int8, int16, int32, int64, float, float16, float32, float64, float128]: + num = (dtype(1), dtype(2)) + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1, 2], dtype=float)) + + def testListListListInt(self): + """ Test an int in a list of a list of a list.""" + num = [[[1]]] + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testListListListFloat(self): + """ Test a float in a list of a list of a list.""" + num = [[[1.0]]] + num_ = _cleanPart(num) + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0], dtype=float)) + + def testListListListInts(self): + """Test 2 lists of ints in a list in a list.""" + num = [[[1,1],[2,2]]] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testListListListFloats(self): + """Test 2 lists of ints in a list in a list.""" + num = [[[1.0,1.0],[2.0,2.0]]] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testListListArray(self): + """List of list of numpy arrays for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = [[array([1,1], dtype=dtype),array([2,2], dtype=dtype)]] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testTupleListArray(self): + """Tuple of list of numpy arrays for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = ([array([1,1], dtype=dtype),array([2,2], dtype=dtype)],) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testListTupleArray(self): + """List of tuple of numpy array for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = [(array([1,1], dtype=dtype),array([2,2], dtype=dtype))] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testTupleTuplesArrays(self): + """Tuple of tuples of numpy arrays for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = ((array([1,1], dtype=dtype),array([2,2], dtype=dtype)), + (array([3,4], dtype=dtype),array([4,4], dtype=dtype))) + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testListTuplesArrays(self): + """List of tuples of numpy arrays for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = [(array([1,1], dtype=dtype),array([2,2], dtype=dtype)), + (array([3,4], dtype=dtype),array([4,4], dtype=dtype))] + num_ = _cleanPart(num) + + assert isinstance(num_, list) + assert np.all([isinstance(part, list) for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + + def testListListArrays(self): + """List of list of numpy arrays for all valid types.""" + for dtype in int, int8, int16, int32, int64, float, float16, float32, float64, float128: + num = [[array([1,1], dtype=dtype),array([2,2], dtype=dtype)], + [array([3,3], dtype=dtype),array([4,4], dtype=dtype)]] + num_ = _cleanPart(num) + + assert len(num_) == 2 + assert np.all([isinstance(part, list) for part in num_]) + assert np.all([len(part) == 2 for part in num_]) + np.testing.assert_array_equal(num_[0][0], array([1.0, 1.0], dtype=float)) + np.testing.assert_array_equal(num_[0][1], array([2.0, 2.0], dtype=float)) + np.testing.assert_array_equal(num_[1][0], array([3.0, 3.0], dtype=float)) + np.testing.assert_array_equal(num_[1][1], array([4.0, 4.0], dtype=float)) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestXferFcnInput) + +if __name__ == "__main__": + unittest.main() diff --git a/control/xferfcn.py b/control/xferfcn.py index 0d7d2f417..982b8c5ba 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -92,12 +92,12 @@ def __init__(self, *args): The default constructor is TransferFunction(num, den), where num and den are lists of lists of arrays containing polynomial coefficients. - To crete a discrete time transfer funtion, use TransferFunction(num, + To create a discrete time transfer funtion, use TransferFunction(num, den, dt). To call the copy constructor, call TransferFunction(sys), where sys is a TransferFunction object (continuous or discrete). """ - + args = deepcopy(args) if len(args) == 2: # The user provided a numerator and a denominator. (num, den) = args @@ -121,55 +121,13 @@ def __init__(self, *args): raise ValueError("Needs 1, 2 or 3 arguments; received %i." % len(args)) - # Make num and den into lists of lists of arrays, if necessary. - # Beware: this is a shallow copy! This should be okay, - # but be careful. - data = [num, den] - for i in range(len(data)): - # Check for a scalar (including 0d ndarray) - if (isinstance(data[i], (int, float, complex, np.number)) or - (isinstance(data[i], ndarray) and data[i].ndim == 0)): - # Convert scalar to list of list of array. - if (isinstance(data[i], int)): - # Convert integers to floats at this point - data[i] = [[array([data[i]], dtype=float)]] - else: - data[i] = [[array([data[i]])]] - elif (isinstance(data[i], (list, tuple, ndarray)) and - isinstance(data[i][0], (int, float, complex, np.number))): - # Convert array to list of list of array. - if (isinstance(data[i][0], int)): - # Convert integers to floats at this point - #! Not sure this covers all cases correctly - data[i] = [[array(data[i], dtype=float)]] - else: - data[i] = [[array(data[i])]] - elif (isinstance(data[i], list) and - isinstance(data[i][0], list) and - isinstance(data[i][0][0], (list, tuple, ndarray)) and - isinstance(data[i][0][0][0], (int, float, complex, - np.number))): - # We might already have the right format. Convert the - # coefficient vectors to arrays, if necessary. - for j in range(len(data[i])): - for k in range(len(data[i][j])): - if (isinstance(data[i][j][k], int)): - data[i][j][k] = array(data[i][j][k], dtype=float) - else: - data[i][j][k] = array(data[i][j][k]) - else: - # If the user passed in anything else, then it's unclear what - # the meaning is. - raise TypeError("The numerator and denominator inputs must be \ -scalars or vectors (for\nSISO), or lists of lists of vectors (for SISO or \ -MIMO).") - [num, den] = data + num = _cleanPart(num) + den = _cleanPart(den) inputs = len(num[0]) outputs = len(num) - # Make sure the numerator and denominator matrices have consistent - # sizes. + # Make sure numerator and denominator matrices have consistent sizes if inputs != len(den[0]): raise ValueError("The numerator has %i input(s), but the \ denominator has %i\ninput(s)." % (inputs, len(den[0]))) @@ -177,8 +135,9 @@ def __init__(self, *args): raise ValueError("The numerator has %i output(s), but the \ denominator has %i\noutput(s)." % (outputs, len(den))) + # Additional checks/updates on structure of the transfer function for i in range(outputs): - # Make sure that each row has the same number of columns. + # Make sure that each row has the same number of columns if len(num[i]) != inputs: raise ValueError("Row 0 of the numerator matrix has %i \ elements, but row %i\nhas %i." % (inputs, i, len(num[i]))) @@ -186,6 +145,7 @@ def __init__(self, *args): raise ValueError("Row 0 of the denominator matrix has %i \ elements, but row %i\nhas %i." % (inputs, i, len(den[i]))) + # Check for zeros in numerator or denominator # TODO: Right now these checks are only done during construction. # It might be worthwhile to think of a way to perform checks if the # user modifies the transfer function after construction. @@ -1358,3 +1318,52 @@ def tfdata(sys): tf = _convertToTransferFunction(sys) return (tf.num, tf.den) + +def _cleanPart(data): + ''' + Return a valid, cleaned up numerator or denominator + for the TransferFunction class. + + Parameters + ---------- + data: numerator or denominator of a transfer function. + + Returns + ------- + data: list of lists of ndarrays, with int converted to float + ''' + valid_types = (int, float, complex, np.number) + valid_collection = (list, tuple, ndarray) + + if (isinstance(data, valid_types) or + (isinstance(data, ndarray) and data.ndim == 0)): + # Data is a scalar (including 0d ndarray) + data = [[array([data])]] + elif (isinstance(data, valid_collection) and + all([isinstance(d, valid_types) for d in data])): + data = [[array(data)]] + elif (isinstance(data, (list, tuple)) and + isinstance(data[0], (list, tuple)) and + (isinstance(data[0][0], valid_collection) and + all([isinstance(d, valid_types) for d in data[0][0]]))): + data = list(data) + for j in range(len(data)): + data[j] = list(data[j]) + for k in range(len(data[j])): + data[j][k] = array(data[j][k]) + else: + # If the user passed in anything else, then it's unclear what + # the meaning is. + raise TypeError("The numerator and denominator inputs must be \ +scalars or vectors (for\nSISO), or lists of lists of vectors (for SISO or \ +MIMO).") + + # Check for coefficients that are ints and convert to floats + for i in range(len(data)): + for j in range(len(data[i])): + for k in range(len(data[i][j])): + if (isinstance(data[i][j][k], (int, np.int))): + data[i][j][k] = float(data[i][j][k]) + + return data +