#!/usr/bin/env python # # xferfcn_test.py - test TransferFunction class # RMM, 30 Mar 2011 (based on TestXferFcn from v0.4a) import unittest import sys as pysys import numpy as np from control.statesp import StateSpace, _convertToStateSpace, rss from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ ss2tf from control.lti import evalfr from control.exception import slycot_check from control.lti import isctime, isdtime from control.dtime import sample_system class TestXferFcn(unittest.TestCase): """These are tests for functionality and correct reporting of the transfer function class. Throughout these tests, we will give different input formats to the xTranferFunction constructor, to try to break it. These tests have been verified in MATLAB.""" # Tests for raising exceptions. def test_constructor_bad_input_type(self): """Give the constructor invalid input types.""" # MIMO requires lists of lists of vectors (not lists of vectors) self.assertRaises( TypeError, TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]]) TransferFunction([[ [0., 1.], [2., 3.] ]], [[ [5., 2.], [3., 0.] ]]) # Single argument of the wrong type self.assertRaises(TypeError, TransferFunction, [1]) # Too many arguments self.assertRaises(ValueError, TransferFunction, 1, 2, 3, 4) # Different numbers of elements in numerator rows self.assertRaises( ValueError, TransferFunction, [ [[0, 1], [2, 3]], [[4, 5]] ], [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ]) self.assertRaises( ValueError, TransferFunction, [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], [ [[6, 7], [4, 5]], [[2, 3]] ]) TransferFunction( # This version is OK [ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ], [ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ]) def test_constructor_inconsistent_dimension(self): """Give constructor numerators, denominators of different sizes.""" self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.], [2., 3.]]]) self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.]], [[2., 3.]]]) self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]]) def test_constructor_inconsistent_columns(self): """Give the constructor inputs that do not have the same number of columns in each row.""" self.assertRaises(ValueError, TransferFunction, 1., [[[1.]], [[2.], [3.]]]) self.assertRaises(ValueError, TransferFunction, [[[1.]], [[2.], [3.]]], 1.) def test_constructor_zero_denominator(self): """Give the constructor a transfer function with a zero denominator.""" self.assertRaises(ValueError, TransferFunction, 1., 0.) self.assertRaises(ValueError, TransferFunction, [[[1.], [2., 3.]], [[-1., 4.], [3., 2.]]], [[[1., 0.], [0.]], [[0., 0.], [2.]]]) def test_add_inconsistent_dimension(self): """Add two transfer function matrices of different sizes.""" sys1 = TransferFunction([[[1., 2.]]], [[[4., 5.]]]) sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]], [[[1., 6.]], [[2., 4.]]]) self.assertRaises(ValueError, sys1.__add__, sys2) self.assertRaises(ValueError, sys1.__sub__, sys2) self.assertRaises(ValueError, sys1.__radd__, sys2) self.assertRaises(ValueError, sys1.__rsub__, sys2) def test_mul_inconsistent_dimension(self): """Multiply two transfer function matrices of incompatible sizes.""" sys1 = TransferFunction([[[1., 2.], [4., 5.]], [[2., 5.], [4., 3.]]], [[[6., 2.], [4., 1.]], [[6., 7.], [2., 4.]]]) sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]], [[[4.]], [[5.]], [[6.]]]) self.assertRaises(ValueError, sys1.__mul__, sys2) self.assertRaises(ValueError, sys2.__mul__, sys1) self.assertRaises(ValueError, sys1.__rmul__, sys2) self.assertRaises(ValueError, sys2.__rmul__, sys1) # Tests for TransferFunction._truncatecoeff def test_truncate_coefficients_non_null_numerator(self): """Remove extraneous zeros in polynomial representations.""" sys1 = TransferFunction([0., 0., 1., 2.], [[[0., 0., 0., 3., 2., 1.]]]) np.testing.assert_array_equal(sys1.num, [[[1., 2.]]]) np.testing.assert_array_equal(sys1.den, [[[3., 2., 1.]]]) def test_truncate_coefficients_null_numerator(self): """Remove extraneous zeros in polynomial representations.""" sys1 = TransferFunction([0., 0., 0.], 1.) np.testing.assert_array_equal(sys1.num, [[[0.]]]) np.testing.assert_array_equal(sys1.den, [[[1.]]]) # Tests for TransferFunction.__neg__ def test_reverse_sign_scalar(self): """Negate a direct feedthrough system.""" sys1 = TransferFunction(2., np.array([-3.])) sys2 = - sys1 np.testing.assert_array_equal(sys2.num, [[[-2.]]]) np.testing.assert_array_equal(sys2.den, [[[-3.]]]) def test_reverse_sign_siso(self): """Negate a SISO system.""" sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1.]) sys2 = - sys1 np.testing.assert_array_equal(sys2.num, [[[-1., -3., -5.]]]) np.testing.assert_array_equal(sys2.den, [[[1., 6., 2., -1.]]]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_reverse_sign_mimo(self): """Negate a MIMO system.""" num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] num3 = [[[-1., -2.], [0., -3.], [-2., 1.]], [[-1.], [-4., 0.], [-1., 4., -3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] sys1 = TransferFunction(num1, den1) sys2 = - sys1 sys3 = TransferFunction(num3, den1) for i in range(sys3.outputs): for j in range(sys3.inputs): np.testing.assert_array_equal(sys2.num[i][j], sys3.num[i][j]) np.testing.assert_array_equal(sys2.den[i][j], sys3.den[i][j]) # Tests for TransferFunction.__add__ def test_add_scalar(self): """Add two direct feedthrough systems.""" sys1 = TransferFunction(1., [[[1.]]]) sys2 = TransferFunction(np.array([2.]), [1.]) sys3 = sys1 + sys2 np.testing.assert_array_equal(sys3.num, 3.) np.testing.assert_array_equal(sys3.den, 1.) def test_add_siso(self): """Add two SISO systems.""" sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[np.array([-1., 3.])]], [[[1., 0., -1.]]]) sys3 = sys1 + sys2 # If sys3.num is [[[0., 20., 4., -8.]]], then this is wrong! np.testing.assert_array_equal(sys3.num, [[[20., 4., -8]]]) np.testing.assert_array_equal(sys3.den, [[[1., 6., 1., -7., -2., 1.]]]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_add_mimo(self): """Add two MIMO systems.""" num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] num2 = [[[0., 0., -1], [2.], [-1., -1.]], [[1., 2.], [-1., -2.], [4.]]] den2 = [[[-1.], [1., 2., 3.], [-1., -1.]], [[-4., -3., 2.], [0., 1.], [1., 0.]]] num3 = [[[3., -3., -6], [5., 6., 9.], [-4., -2., 2]], [[3., 2., -3., 2], [-2., -3., 7., 2.], [1., -4., 3., 4]]] den3 = [[[3., -2., -4.], [1., 2., 3., 0., 0.], [-2., -1., 1.]], [[-12., -9., 6., 0., 0.], [2., -1., -1.], [1., 0.]]] sys1 = TransferFunction(num1, den1) sys2 = TransferFunction(num2, den2) sys3 = sys1 + sys2 for i in range(sys3.outputs): for j in range(sys3.inputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) # Tests for TransferFunction.__sub__ def test_subtract_scalar(self): """Subtract two direct feedthrough systems.""" sys1 = TransferFunction(1., [[[1.]]]) sys2 = TransferFunction(np.array([2.]), [1.]) sys3 = sys1 - sys2 np.testing.assert_array_equal(sys3.num, -1.) np.testing.assert_array_equal(sys3.den, 1.) def test_subtract_siso(self): """Subtract two SISO systems.""" sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[np.array([-1., 3.])]], [[[1., 0., -1.]]]) sys3 = sys1 - sys2 sys4 = sys2 - sys1 np.testing.assert_array_equal(sys3.num, [[[2., 6., -12., -10., -2.]]]) np.testing.assert_array_equal(sys3.den, [[[1., 6., 1., -7., -2., 1.]]]) np.testing.assert_array_equal(sys4.num, [[[-2., -6., 12., 10., 2.]]]) np.testing.assert_array_equal(sys4.den, [[[1., 6., 1., -7., -2., 1.]]]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_subtract_mimo(self): """Subtract two MIMO systems.""" num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] num2 = [[[0., 0., -1], [2.], [-1., -1.]], [[1., 2.], [-1., -2.], [4.]]] den2 = [[[-1.], [1., 2., 3.], [-1., -1.]], [[-4., -3., 2.], [0., 1.], [1., 0.]]] num3 = [[[-3., 1., 2.], [1., 6., 9.], [0.]], [[-3., -10., -3., 2], [2., 3., 1., -2], [1., -4., 3., -4]]] den3 = [[[3., -2., -4], [1., 2., 3., 0., 0.], [1]], [[-12., -9., 6., 0., 0.], [2., -1., -1], [1., 0.]]] sys1 = TransferFunction(num1, den1) sys2 = TransferFunction(num2, den2) sys3 = sys1 - sys2 for i in range(sys3.outputs): for j in range(sys3.inputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) # Tests for TransferFunction.__mul__ def test_multiply_scalar(self): """Multiply two direct feedthrough systems.""" sys1 = TransferFunction(2., [1.]) sys2 = TransferFunction(1., 4.) sys3 = sys1 * sys2 sys4 = sys1 * sys2 np.testing.assert_array_equal(sys3.num, [[[2.]]]) np.testing.assert_array_equal(sys3.den, [[[4.]]]) np.testing.assert_array_equal(sys3.num, sys4.num) np.testing.assert_array_equal(sys3.den, sys4.den) def test_multiply_siso(self): """Multiply two SISO systems.""" sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]]) sys3 = sys1 * sys2 sys4 = sys2 * sys1 np.testing.assert_array_equal(sys3.num, [[[-1., 0., 4., 15.]]]) np.testing.assert_array_equal(sys3.den, [[[1., 6., 1., -7., -2., 1.]]]) np.testing.assert_array_equal(sys3.num, sys4.num) np.testing.assert_array_equal(sys3.den, sys4.den) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_multiply_mimo(self): """Multiply two MIMO systems.""" num1 = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den1 = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] num2 = [[[0., 1., 2.]], [[1., -5.]], [[-2., 1., 4.]]] den2 = [[[1., 0., 0., 0.]], [[-2., 1., 3.]], [[4., -1., -1., 0.]]] num3 = [[[-24., 52., -14., 245., -490., -115., 467., -95., -56., 12., 0., 0., 0.]], [[24., -132., 138., 345., -768., -106., 510., 41., -79., -69., -23., 17., 6., 0.]]] den3 = [[[48., -92., -84., 183., 44., -97., -2., 12., 0., 0., 0., 0., 0., 0.]], [[-48., 60., 84., -81., -45., 21., 9., 0., 0., 0., 0., 0., 0.]]] sys1 = TransferFunction(num1, den1) sys2 = TransferFunction(num2, den2) sys3 = sys1 * sys2 for i in range(sys3.outputs): for j in range(sys3.inputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) # Tests for TransferFunction.__div__ def test_divide_scalar(self): """Divide two direct feedthrough systems.""" sys1 = TransferFunction(np.array([3.]), -4.) sys2 = TransferFunction(5., 2.) sys3 = sys1 / sys2 np.testing.assert_array_equal(sys3.num, [[[6.]]]) np.testing.assert_array_equal(sys3.den, [[[-20.]]]) def test_divide_siso(self): """Divide two SISO systems.""" sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]]) sys3 = sys1 / sys2 sys4 = sys2 / sys1 np.testing.assert_array_equal(sys3.num, [[[1., 3., 4., -3., -5.]]]) np.testing.assert_array_equal(sys3.den, [[[-1., -3., 16., 7., -3.]]]) np.testing.assert_array_equal(sys4.num, sys3.den) np.testing.assert_array_equal(sys4.den, sys3.num) def test_div(self): # Make sure that sampling times work correctly sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], True) sys3 = sys1 / sys2 self.assertEqual(sys3.dt, True) sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], 0.5) sys3 = sys1 / sys2 self.assertEqual(sys3.dt, 0.5) sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) self.assertRaises(ValueError, TransferFunction.__truediv__, sys1, sys2) sys1 = sample_system(rss(4, 1, 1), 0.5) sys3 = TransferFunction.__rtruediv__(sys2, sys1) self.assertEqual(sys3.dt, 0.5) def test_pow(self): sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1]) self.assertRaises(ValueError, TransferFunction.__pow__, sys1, 0.5) def test_slice(self): sys = TransferFunction( [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ]) sys1 = sys[1:, 1:] self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) sys2 = sys[:2, :2] self.assertEqual((sys2.inputs, sys2.outputs), (2, 2)) sys = TransferFunction( [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5) sys1 = sys[1:, 1:] self.assertEqual((sys1.inputs, sys1.outputs), (2, 1)) self.assertEqual(sys1.dt, 0.5) def test_evalfr_siso(self): """Evaluate the frequency response of a SISO system at one frequency.""" sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) np.testing.assert_array_almost_equal(evalfr(sys, 1j), np.array([[-0.5 - 0.5j]])) np.testing.assert_array_almost_equal( evalfr(sys, 32j), np.array([[0.00281959302585077 - 0.030628473607392j]])) # Test call version as well np.testing.assert_almost_equal(sys(1.j), -0.5 - 0.5j) np.testing.assert_almost_equal( sys(32.j), 0.00281959302585077 - 0.030628473607392j) # Test internal version (with real argument) np.testing.assert_array_almost_equal( sys._evalfr(1.), np.array([[-0.5 - 0.5j]])) np.testing.assert_array_almost_equal( sys._evalfr(32.), np.array([[0.00281959302585077 - 0.030628473607392j]])) # This test only works in Python 3 due to a conflict with the same # warning type in other test modules (frd_test.py). See # https://bugs.python.org/issue4180 for more details @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") def test_evalfr_deprecated(self): sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) # Deprecated version of the call (should generate warning) import warnings with warnings.catch_warnings(): # Make warnings generate an exception warnings.simplefilter('error') # Make sure that we get a pending deprecation warning self.assertRaises(PendingDeprecationWarning, sys.evalfr, 1.) @unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+") def test_evalfr_dtime(self): sys = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1) np.testing.assert_array_almost_equal(sys(1j), -0.5 - 0.5j) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_evalfr_mimo(self): """Evaluate the frequency response of a MIMO system at one frequency.""" num = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] sys = TransferFunction(num, den) resp = [[0.147058823529412 + 0.0882352941176471j, -0.75, 1.], [-0.083333333333333, -0.188235294117647 - 0.847058823529412j, -1. - 8.j]] np.testing.assert_array_almost_equal(sys._evalfr(2.), resp) # Test call version as well np.testing.assert_array_almost_equal(sys(2.j), resp) def test_freqresp_siso(self): """Evaluate the magnitude and phase of a SISO system at multiple frequencies.""" sys = TransferFunction([1., 3., 5], [1., 6., 2., -1]) truemag = [[[4.63507337473906, 0.707106781186548, 0.0866592803995351]]] truephase = [[[-2.89596891081488, -2.35619449019234, -1.32655885133871]]] trueomega = [0.1, 1., 10.] mag, phase, omega = sys.freqresp(trueomega) np.testing.assert_array_almost_equal(mag, truemag) np.testing.assert_array_almost_equal(phase, truephase) np.testing.assert_array_almost_equal(omega, trueomega) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_freqresp_mimo(self): """Evaluate the magnitude and phase of a MIMO system at multiple frequencies.""" num = [[[1., 2.], [0., 3.], [2., -1.]], [[1.], [4., 0.], [1., -4., 3.]]] den = [[[-3., 2., 4.], [1., 0., 0.], [2., -1.]], [[3., 0., .0], [2., -1., -1.], [1.]]] sys = TransferFunction(num, den) true_omega = [0.1, 1., 10.] true_mag = [[[0.49628709, 0.30714755, 0.03347381], [300., 3., 0.03], [1., 1., 1.]], [[33.333333, 0.33333333, 0.00333333], [0.39028569, 1.26491106, 0.19875914], [3.01663720, 4.47213595, 104.92378186]]] true_phase = [[[3.7128711e-4, 0.18534794, 1.30770596], [-np.pi, -np.pi, -np.pi], [0., 0., 0.]], [[-np.pi, -np.pi, -np.pi], [-1.66852323, -1.89254688, -1.62050658], [-0.13298964, -1.10714871, -2.75046720]]] mag, phase, omega = sys.freqresp(true_omega) np.testing.assert_array_almost_equal(mag, true_mag) np.testing.assert_array_almost_equal(phase, true_phase) np.testing.assert_array_equal(omega, true_omega) # Tests for TransferFunction.pole and TransferFunction.zero. def test_common_den(self): """ Test the helper function to compute common denomitators.""" # _common_den() computes the common denominator per input/column. # The testing columns are: # 0: no common poles # 1: regular common poles # 2: poles with multiplicity, # 3: complex poles # 4: complex poles below threshold eps = np.finfo(float).eps tol_imag = np.sqrt(eps*5*2*2)*0.9 numin = [[[1.], [1.], [1.], [1.], [1.]], [[1.], [1.], [1.], [1.], [1.]]] denin = [[[1., 3., 2.], # 0: poles: [-1, -2] [1., 6., 11., 6.], # 1: poles: [-1, -2, -3] [1., 6., 11., 6.], # 2: poles: [-1, -2, -3] [1., 6., 11., 6.], # 3: poles: [-1, -2, -3] [1., 6., 11., 6.]], # 4: poles: [-1, -2, -3], [[1., 12., 47., 60.], # 0: poles: [-3, -4, -5] [1., 9., 26., 24.], # 1: poles: [-2, -3, -4] [1., 7., 16., 12.], # 2: poles: [-2, -2, -3] [1., 7., 17., 15.], # 3: poles: [-2+1J, -2-1J, -3], np.poly([-2 + tol_imag * 1J, -2 - tol_imag * 1J, -3])]] numref = np.array([ [[0., 0., 1., 12., 47., 60.], [0., 0., 0., 1., 4., 0.], [0., 0., 0., 1., 2., 0.], [0., 0., 0., 1., 4., 5.], [0., 0., 0., 1., 2., 0.]], [[0., 0., 0., 1., 3., 2.], [0., 0., 0., 1., 1., 0.], [0., 0., 0., 1., 1., 0.], [0., 0., 0., 1., 3., 2.], [0., 0., 0., 1., 1., 0.]]]) denref = np.array( [[1., 15., 85., 225., 274., 120.], [1., 10., 35., 50., 24., 0.], [1., 8., 23., 28., 12., 0.], [1., 10., 40., 80., 79., 30.], [1., 8., 23., 28., 12., 0.]]) sys = TransferFunction(numin, denin) num, den, denorder = sys._common_den() np.testing.assert_array_almost_equal(num[:2, :, :], numref) np.testing.assert_array_almost_equal(num[2:, :, :], np.zeros((3, 5, 6))) np.testing.assert_array_almost_equal(den, denref) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_pole_mimo(self): """Test for correct MIMO poles.""" sys = TransferFunction( [[[1.], [1.]], [[1.], [1.]]], [[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]]) p = sys.pole() np.testing.assert_array_almost_equal(p, [-2., -2., -7., -3., -2.]) def test_double_cancelling_poles_siso(self): H = TransferFunction([1, 1], [1, 2, 1]) p = H.pole() np.testing.assert_array_almost_equal(p, [-1, -1]) # Tests for TransferFunction.feedback def test_feedback_siso(self): """Test for correct SISO transfer function feedback.""" sys1 = TransferFunction([-1., 4.], [1., 3., 5.]) sys2 = TransferFunction([2., 3., 0.], [1., -3., 4., 0]) sys3 = sys1.feedback(sys2) sys4 = sys1.feedback(sys2, 1) np.testing.assert_array_equal(sys3.num, [[[-1., 7., -16., 16., 0.]]]) np.testing.assert_array_equal(sys3.den, [[[1., 0., -2., 2., 32., 0.]]]) np.testing.assert_array_equal(sys4.num, [[[-1., 7., -16., 16., 0.]]]) np.testing.assert_array_equal(sys4.den, [[[1., 0., 2., -8., 8., 0.]]]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_convert_to_transfer_function(self): """Test for correct state space to transfer function conversion.""" A = [[1., -2.], [-3., 4.]] B = [[6., 5.], [4., 3.]] C = [[1., -2.], [3., -4.], [5., -6.]] D = [[1., 0.], [0., 1.], [1., 0.]] sys = StateSpace(A, B, C, D) tfsys = _convert_to_transfer_function(sys) num = [[np.array([1., -7., 10.]), np.array([-1., 10.])], [np.array([2., -8.]), np.array([1., -2., -8.])], [np.array([1., 1., -30.]), np.array([7., -22.])]] den = [[np.array([1., -5., -2.]) for _ in range(sys.inputs)] for _ in range(sys.outputs)] for i in range(sys.outputs): for j in range(sys.inputs): np.testing.assert_array_almost_equal(tfsys.num[i][j], num[i][j]) np.testing.assert_array_almost_equal(tfsys.den[i][j], den[i][j]) def test_minreal(self): """Try the minreal function, and also test easy entry by creation of a Laplace variable s""" s = TransferFunction([1, 0], [1]) h = (s + 1) * (s + 2.00000000001) / (s + 2) / (s**2 + s + 1) hm = h.minreal() hr = (s + 1) / (s**2 + s + 1) np.testing.assert_array_almost_equal(hm.num[0][0], hr.num[0][0]) np.testing.assert_array_almost_equal(hm.den[0][0], hr.den[0][0]) np.testing.assert_equal(hm.dt, hr.dt) def test_minreal_2(self): """This one gave a problem, due to poly([]) giving simply 1 instead of numpy.array([1])""" s = TransferFunction([1, 0], [1]) G = 6205/(s*(s**2 + 13*s + 1281)) Heq = G.feedback(1) H1 = 1/(s+5) H2a = Heq/H1 H2b = H2a.minreal() hr = 6205/(s**2+8*s+1241) np.testing.assert_array_almost_equal(H2b.num[0][0], hr.num[0][0]) np.testing.assert_array_almost_equal(H2b.den[0][0], hr.den[0][0]) np.testing.assert_equal(H2b.dt, hr.dt) def test_minreal_3(self): """Regression test for minreal of tf([1,1],[1,1])""" g = TransferFunction([1,1],[1,1]).minreal() np.testing.assert_array_almost_equal(1.0, g.num[0][0]) np.testing.assert_array_almost_equal(1.0, g.den[0][0]) np.testing.assert_equal(None, g.dt) def test_minreal_4(self): """Check minreal on discrete TFs.""" T = 0.01 z = TransferFunction([1, 0], [1], T) h = (z - 1.00000000001) * (z + 1.0000000001) / (z**2 - 1) hm = h.minreal() hr = TransferFunction([1], [1], T) np.testing.assert_array_almost_equal(hm.num[0][0], hr.num[0][0]) np.testing.assert_equal(hr.dt, hm.dt) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_state_space_conversion_mimo(self): """Test conversion of a single input, two-output state-space system against the same TF""" s = TransferFunction([1, 0], [1]) b0 = 0.2 b1 = 0.1 b2 = 0.5 a0 = 2.3 a1 = 6.3 a2 = 3.6 a3 = 1.0 h = (b0 + b1*s + b2*s**2)/(a0 + a1*s + a2*s**2 + a3*s**3) H = TransferFunction([[h.num[0][0]], [(h*s).num[0][0]]], [[h.den[0][0]], [h.den[0][0]]]) sys = _convertToStateSpace(H) H2 = _convert_to_transfer_function(sys) np.testing.assert_array_almost_equal(H.num[0][0], H2.num[0][0]) np.testing.assert_array_almost_equal(H.den[0][0], H2.den[0][0]) np.testing.assert_array_almost_equal(H.num[1][0], H2.num[1][0]) np.testing.assert_array_almost_equal(H.den[1][0], H2.den[1][0]) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_indexing(self): tm = ss2tf(rss(5, 3, 3)) # scalar indexing sys01 = tm[0, 1] np.testing.assert_array_almost_equal(sys01.num[0][0], tm.num[0][1]) np.testing.assert_array_almost_equal(sys01.den[0][0], tm.den[0][1]) # slice indexing sys = tm[:2, 1:3] np.testing.assert_array_almost_equal(sys.num[0][0], tm.num[0][1]) np.testing.assert_array_almost_equal(sys.den[0][0], tm.den[0][1]) np.testing.assert_array_almost_equal(sys.num[0][1], tm.num[0][2]) np.testing.assert_array_almost_equal(sys.den[0][1], tm.den[0][2]) np.testing.assert_array_almost_equal(sys.num[1][0], tm.num[1][1]) np.testing.assert_array_almost_equal(sys.den[1][0], tm.den[1][1]) np.testing.assert_array_almost_equal(sys.num[1][1], tm.num[1][2]) np.testing.assert_array_almost_equal(sys.den[1][1], tm.den[1][2]) def test_matrix_multiply(self): """MIMO transfer functions should be multiplyable by constant matrices""" s = TransferFunction([1, 0], [1]) b0 = 0.2 b1 = 0.1 b2 = 0.5 a0 = 2.3 a1 = 6.3 a2 = 3.6 a3 = 1.0 h = (b0 + b1*s + b2*s**2)/(a0 + a1*s + a2*s**2 + a3*s**3) H = TransferFunction([[h.num[0][0]], [(h*s).num[0][0]]], [[h.den[0][0]], [h.den[0][0]]]) H1 = (np.matrix([[1.0, 0]])*H).minreal() H2 = (np.matrix([[0, 1.0]])*H).minreal() np.testing.assert_array_almost_equal(H.num[0][0], H1.num[0][0]) np.testing.assert_array_almost_equal(H.den[0][0], H1.den[0][0]) np.testing.assert_array_almost_equal(H.num[1][0], H2.num[0][0]) np.testing.assert_array_almost_equal(H.den[1][0], H2.den[0][0]) def test_dcgain_cont(self): """Test DC gain for continuous-time transfer functions""" sys = TransferFunction(6, 3) np.testing.assert_equal(sys.dcgain(), 2) sys2 = TransferFunction(6, [1, 3]) np.testing.assert_equal(sys2.dcgain(), 2) sys3 = TransferFunction(6, [1, 0]) np.testing.assert_equal(sys3.dcgain(), np.inf) num = [[[15], [21], [33]], [[10], [14], [22]]] den = [[[1, 3], [2, 3], [3, 3]], [[1, 5], [2, 7], [3, 11]]] sys4 = TransferFunction(num, den) expected = [[5, 7, 11], [2, 2, 2]] np.testing.assert_array_equal(sys4.dcgain(), expected) def test_dcgain_discr(self): """Test DC gain for discrete-time transfer functions""" # static gain sys = TransferFunction(6, 3, True) np.testing.assert_equal(sys.dcgain(), 2) # averaging filter sys = TransferFunction(0.5, [1, -0.5], True) np.testing.assert_almost_equal(sys.dcgain(), 1) # differencer sys = TransferFunction(1, [1, -1], True) np.testing.assert_equal(sys.dcgain(), np.inf) # summer # causes a RuntimeWarning due to the divide by zero sys = TransferFunction([1, -1], [1], True) np.testing.assert_equal(sys.dcgain(), 0) def test_ss2tf(self): A = np.array([[-4, -1], [-1, -4]]) B = np.array([[1], [3]]) C = np.array([[3, 1]]) D = 0 sys = ss2tf(A, B, C, D) true_sys = TransferFunction([6., 14.], [1., 8., 15.]) np.testing.assert_almost_equal(sys.num, true_sys.num) np.testing.assert_almost_equal(sys.den, true_sys.den) def test_class_constants(self): # Make sure that the 's' variable is defined properly s = TransferFunction.s G = (s + 1)/(s**2 + 2*s + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) self.assertTrue(isctime(G, strict=True)) # Make sure that the 'z' variable is defined properly z = TransferFunction.z G = (z + 1)/(z**2 + 2*z + 1) np.testing.assert_array_almost_equal(G.num, [[[1, 1]]]) np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]]) self.assertTrue(isdtime(G, strict=True)) def test_printing(self): # SISO, continuous time sys = ss2tf(rss(4, 1, 1)) self.assertTrue(isinstance(str(sys), str)) self.assertTrue(isinstance(sys._repr_latex_(), str)) # SISO, discrete time sys = sample_system(sys, 1) self.assertTrue(isinstance(str(sys), str)) self.assertTrue(isinstance(sys._repr_latex_(), str)) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_printing_mimo(self): # MIMO, continuous time sys = ss2tf(rss(4, 2, 3)) self.assertTrue(isinstance(str(sys), str)) self.assertTrue(isinstance(sys._repr_latex_(), str)) @unittest.skipIf(not slycot_check(), "slycot not installed") def test_size_mismatch(self): sys1 = ss2tf(rss(2, 2, 2)) # Different number of inputs sys2 = ss2tf(rss(3, 1, 2)) self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) # Different number of outputs sys2 = ss2tf(rss(3, 2, 1)) self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2) # Inputs and outputs don't match self.assertRaises(ValueError, TransferFunction.__mul__, sys2, sys1) # Feedback mismatch (MIMO not implemented) self.assertRaises(NotImplementedError, TransferFunction.feedback, sys2, sys1) def test_latex_repr(self): """ Test latex printout for TransferFunction """ Hc = TransferFunction([1e-5, 2e5, 3e-4], [1.2e34, 2.3e-4, 2.3e-45]) Hd = TransferFunction([1e-5, 2e5, 3e-4], [1.2e34, 2.3e-4, 2.3e-45], .1) # TODO: make the multiplication sign configurable expmul = r'\times' for var, H, suffix in zip(['s', 'z'], [Hc, Hd], ['', r'\quad dt = 0.1']): ref = (r'$$\frac{' r'1 ' + expmul + ' 10^{-5} ' + var + '^2 ' r'+ 2 ' + expmul + ' 10^{5} ' + var + ' + 0.0003' r'}{' r'1.2 ' + expmul + ' 10^{34} ' + var + '^2 ' r'+ 0.00023 ' + var + ' ' r'+ 2.3 ' + expmul + ' 10^{-45}' r'}' + suffix + '$$') self.assertEqual(H._repr_latex_(), ref) def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestXferFcn) if __name__ == "__main__": unittest.main()