Skip to content

Commit 9f7fce5

Browse files
committed
add unit tests for xferfcn converage
1 parent af49bd6 commit 9f7fce5

3 files changed

Lines changed: 153 additions & 32 deletions

File tree

control/tests/discrete_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def test_sample_system(self):
340340

341341
# Check errors
342342
self.assertRaises(ValueError, sample_system, self.siso_ss1d, 1)
343+
self.assertRaises(ValueError, sample_system, self.siso_tf1d, 1)
343344
self.assertRaises(ValueError, sample_system, self.siso_ss1, 1, 'unknown')
344345

345346
def test_sample_ss(self):

control/tests/xferfcn_test.py

Lines changed: 150 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import sys as pysys
88
import numpy as np
99
from control.statesp import StateSpace, _convertToStateSpace, rss
10-
from control.xferfcn import TransferFunction, _convert_to_transfer_function, ss2tf
10+
from control.xferfcn import TransferFunction, _convert_to_transfer_function, \
11+
ss2tf
1112
from control.lti import evalfr
1213
from control.exception import slycot_check
14+
from control.dtime import sample_system
1315
# from control.lti import isdtime
1416

1517

@@ -24,23 +26,53 @@ class TestXferFcn(unittest.TestCase):
2426
def test_constructor_bad_input_type(self):
2527
"""Give the constructor invalid input types."""
2628

27-
self.assertRaises(TypeError, TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]])
29+
# MIMO requires lists of lists of vectors (not lists of vectors)
30+
self.assertRaises(
31+
TypeError,
32+
TransferFunction, [[0., 1.], [2., 3.]], [[5., 2.], [3., 0.]])
33+
TransferFunction([[ [0., 1.], [2., 3.] ]], [[ [5., 2.], [3., 0.] ]])
34+
35+
# Single argument of the wrong type
36+
self.assertRaises(TypeError, TransferFunction, [1])
37+
38+
# Too many arguments
39+
self.assertRaises(ValueError, TransferFunction, 1, 2, 3, 4)
40+
41+
# Different numbers of elements in numerator rows
42+
self.assertRaises(
43+
ValueError,
44+
TransferFunction, [ [[0, 1], [2, 3]],
45+
[[4, 5]] ],
46+
[ [[6, 7], [4, 5]],
47+
[[2, 3], [0, 1]] ])
48+
self.assertRaises(
49+
ValueError,
50+
TransferFunction, [ [[0, 1], [2, 3]],
51+
[[4, 5], [6, 7]] ],
52+
[ [[6, 7], [4, 5]],
53+
[[2, 3]] ])
54+
TransferFunction( # This version is OK
55+
[ [[0, 1], [2, 3]], [[4, 5], [6, 7]] ],
56+
[ [[6, 7], [4, 5]], [[2, 3], [0, 1]] ])
2857

2958
def test_constructor_inconsistent_dimension(self):
30-
"""Give the constructor a numerator and denominator of different
31-
sizes."""
59+
"""Give constructor numerators, denominators of different sizes."""
3260

33-
self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.], [2., 3.]]])
34-
self.assertRaises(ValueError, TransferFunction, [[[1.]]], [[[1.]], [[2., 3.]]])
35-
self.assertRaises(ValueError, TransferFunction, [[[1.]]],
36-
[[[1.], [1., 2.]], [[5., 2.], [2., 3.]]])
61+
self.assertRaises(ValueError, TransferFunction,
62+
[[[1.]]], [[[1.], [2., 3.]]])
63+
self.assertRaises(ValueError, TransferFunction,
64+
[[[1.]]], [[[1.]], [[2., 3.]]])
65+
self.assertRaises(ValueError, TransferFunction,
66+
[[[1.]]], [[[1.], [1., 2.]], [[5., 2.], [2., 3.]]])
3767

3868
def test_constructor_inconsistent_columns(self):
3969
"""Give the constructor inputs that do not have the same number of
4070
columns in each row."""
4171

42-
self.assertRaises(ValueError, TransferFunction, 1., [[[1.]], [[2.], [3.]]])
43-
self.assertRaises(ValueError, TransferFunction, [[[1.]], [[2.], [3.]]], 1.)
72+
self.assertRaises(ValueError, TransferFunction,
73+
1., [[[1.]], [[2.], [3.]]])
74+
self.assertRaises(ValueError, TransferFunction,
75+
[[[1.]], [[2.], [3.]]], 1.)
4476

4577
def test_constructor_zero_denominator(self):
4678
"""Give the constructor a transfer function with a zero denominator."""
@@ -54,7 +86,8 @@ def test_add_inconsistent_dimension(self):
5486
"""Add two transfer function matrices of different sizes."""
5587

5688
sys1 = TransferFunction([[[1., 2.]]], [[[4., 5.]]])
57-
sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]], [[[1., 6.]], [[2., 4.]]])
89+
sys2 = TransferFunction([[[4., 3.]], [[1., 2.]]],
90+
[[[1., 6.]], [[2., 4.]]])
5891
self.assertRaises(ValueError, sys1.__add__, sys2)
5992
self.assertRaises(ValueError, sys1.__sub__, sys2)
6093
self.assertRaises(ValueError, sys1.__radd__, sys2)
@@ -65,7 +98,8 @@ def test_mul_inconsistent_dimension(self):
6598

6699
sys1 = TransferFunction([[[1., 2.], [4., 5.]], [[2., 5.], [4., 3.]]],
67100
[[[6., 2.], [4., 1.]], [[6., 7.], [2., 4.]]])
68-
sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]], [[[4.]], [[5.]], [[6.]]])
101+
sys2 = TransferFunction([[[1.]], [[2.]], [[3.]]],
102+
[[[4.]], [[5.]], [[6.]]])
69103
self.assertRaises(ValueError, sys1.__mul__, sys2)
70104
self.assertRaises(ValueError, sys2.__mul__, sys1)
71105
self.assertRaises(ValueError, sys1.__rmul__, sys2)
@@ -313,25 +347,67 @@ def test_divide_siso(self):
313347
np.testing.assert_array_equal(sys4.num, sys3.den)
314348
np.testing.assert_array_equal(sys4.den, sys3.num)
315349

350+
def test_div(self):
351+
# Make sure that sampling times work correctly
352+
sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1])
353+
sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], True)
354+
sys3 = sys1 / sys2
355+
self.assertEqual(sys3.dt, True)
356+
357+
sys2 = TransferFunction([[[-1., 3.]]], [[[1., 0., -1.]]], 0.5)
358+
sys3 = sys1 / sys2
359+
self.assertEqual(sys3.dt, 0.5)
360+
361+
sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1)
362+
self.assertRaises(ValueError, TransferFunction.__truediv__, sys1, sys2)
363+
364+
sys1 = sample_system(rss(4, 1, 1), 0.5)
365+
sys3 = TransferFunction.__rtruediv__(sys2, sys1)
366+
self.assertEqual(sys3.dt, 0.5)
367+
368+
def test_pow(self):
369+
sys1 = TransferFunction([1., 3., 5], [1., 6., 2., -1])
370+
self.assertRaises(ValueError, TransferFunction.__pow__, sys1, 0.5)
371+
372+
def test_slice(self):
373+
sys = TransferFunction(
374+
[ [ [1], [2], [3]], [ [3], [4], [5]] ],
375+
[ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ])
376+
sys1 = sys[1:, 1:]
377+
self.assertEqual((sys1.inputs, sys1.outputs), (2, 1))
378+
379+
sys2 = sys[:2, :2]
380+
self.assertEqual((sys2.inputs, sys2.outputs), (2, 2))
381+
382+
sys = TransferFunction(
383+
[ [ [1], [2], [3]], [ [3], [4], [5]] ],
384+
[ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5)
385+
sys1 = sys[1:, 1:]
386+
self.assertEqual((sys1.inputs, sys1.outputs), (2, 1))
387+
self.assertEqual(sys1.dt, 0.5)
388+
316389
def test_evalfr_siso(self):
317390
"""Evaluate the frequency response of a SISO system at one frequency."""
318391

319392
sys = TransferFunction([1., 3., 5], [1., 6., 2., -1])
320393

321394
np.testing.assert_array_almost_equal(evalfr(sys, 1j),
322395
np.array([[-0.5 - 0.5j]]))
323-
np.testing.assert_array_almost_equal(evalfr(sys, 32j),
324-
np.array([[0.00281959302585077 - 0.030628473607392j]]))
396+
np.testing.assert_array_almost_equal(
397+
evalfr(sys, 32j),
398+
np.array([[0.00281959302585077 - 0.030628473607392j]]))
325399

326400
# Test call version as well
327401
np.testing.assert_almost_equal(sys(1.j), -0.5 - 0.5j)
328-
np.testing.assert_almost_equal(sys(32.j), 0.00281959302585077 - 0.030628473607392j)
402+
np.testing.assert_almost_equal(
403+
sys(32.j), 0.00281959302585077 - 0.030628473607392j)
329404

330405
# Test internal version (with real argument)
331-
np.testing.assert_array_almost_equal(sys._evalfr(1.),
332-
np.array([[-0.5 - 0.5j]]))
333-
np.testing.assert_array_almost_equal(sys._evalfr(32.),
334-
np.array([[0.00281959302585077 - 0.030628473607392j]]))
406+
np.testing.assert_array_almost_equal(
407+
sys._evalfr(1.), np.array([[-0.5 - 0.5j]]))
408+
np.testing.assert_array_almost_equal(
409+
sys._evalfr(32.),
410+
np.array([[0.00281959302585077 - 0.030628473607392j]]))
335411

336412
# This test only works in Python 3 due to a conflict with the same
337413
# warning type in other test modules (frd_test.py). See
@@ -349,6 +425,11 @@ def test_evalfr_deprecated(self):
349425
# Make sure that we get a pending deprecation warning
350426
self.assertRaises(PendingDeprecationWarning, sys.evalfr, 1.)
351427

428+
@unittest.skipIf(pysys.version_info < (3, 0), "test requires Python 3+")
429+
def test_evalfr_dtime(self):
430+
sys = TransferFunction([1., 3., 5], [1., 6., 2., -1], 0.1)
431+
np.testing.assert_array_almost_equal(sys(1j), -0.5 - 0.5j)
432+
352433
@unittest.skipIf(not slycot_check(), "slycot not installed")
353434
def test_evalfr_mimo(self):
354435
"""Evaluate the frequency response of a MIMO system at one frequency."""
@@ -368,7 +449,8 @@ def test_evalfr_mimo(self):
368449
np.testing.assert_array_almost_equal(sys(2.j), resp)
369450

370451
def test_freqresp_siso(self):
371-
"""Evaluate the magnitude and phase of a SISO system at multiple frequencies."""
452+
"""Evaluate the magnitude and phase of a SISO system at
453+
multiple frequencies."""
372454

373455
sys = TransferFunction([1., 3., 5], [1., 6., 2., -1])
374456

@@ -385,7 +467,8 @@ def test_freqresp_siso(self):
385467

386468
@unittest.skipIf(not slycot_check(), "slycot not installed")
387469
def test_freqresp_mimo(self):
388-
"""Evaluate the magnitude and phase of a MIMO system at multiple frequencies."""
470+
"""Evaluate the magnitude and phase of a MIMO system at
471+
multiple frequencies."""
389472

390473
num = [[[1., 2.], [0., 3.], [2., -1.]],
391474
[[1.], [4., 0.], [1., -4., 3.]]]
@@ -394,16 +477,17 @@ def test_freqresp_mimo(self):
394477
sys = TransferFunction(num, den)
395478

396479
true_omega = [0.1, 1., 10.]
397-
true_mag = [[[0.496287094505259, 0.307147558416976, 0.0334738176210382],
480+
true_mag = [[[0.49628709, 0.30714755, 0.03347381],
398481
[300., 3., 0.03], [1., 1., 1.]],
399-
[[33.3333333333333, 0.333333333333333, 0.00333333333333333],
400-
[0.390285696125482, 1.26491106406735, 0.198759144198533],
401-
[3.01663720059274, 4.47213595499958, 104.92378186093]]]
402-
true_phase = [[[3.7128711165168e-4, 0.185347949995695, 1.30770596539255],
403-
[-np.pi, -np.pi, -np.pi], [0., 0., 0.]],
482+
[[33.333333, 0.33333333, 0.00333333],
483+
[0.39028569, 1.26491106, 0.19875914],
484+
[3.01663720, 4.47213595, 104.92378186]]]
485+
true_phase = [[[3.7128711e-4, 0.18534794,
486+
1.30770596], [-np.pi, -np.pi, -np.pi],
487+
[0., 0., 0.]],
404488
[[-np.pi, -np.pi, -np.pi],
405-
[-1.66852323415362, -1.89254688119154, -1.62050658356412],
406-
[-0.132989648369409, -1.1071487177940, -2.7504672066207]]]
489+
[-1.66852323, -1.89254688, -1.62050658],
490+
[-0.13298964, -1.10714871, -2.75046720]]]
407491

408492
mag, phase, omega = sys.freqresp(true_omega)
409493

@@ -417,8 +501,9 @@ def test_freqresp_mimo(self):
417501
def test_pole_mimo(self):
418502
"""Test for correct MIMO poles."""
419503

420-
sys = TransferFunction([[[1.], [1.]], [[1.], [1.]]],
421-
[[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]])
504+
sys = TransferFunction(
505+
[[[1.], [1.]], [[1.], [1.]]],
506+
[[[1., 2.], [1., 3.]], [[1., 4., 4.], [1., 9., 14.]]])
422507
p = sys.pole()
423508

424509
np.testing.assert_array_almost_equal(p, [-2., -2., -7., -3., -2.])
@@ -619,6 +704,40 @@ def test_ss2tf(self):
619704
np.testing.assert_almost_equal(sys.num, true_sys.num)
620705
np.testing.assert_almost_equal(sys.den, true_sys.den)
621706

707+
def test_printing(self):
708+
# SISO, continuous time
709+
sys = ss2tf(rss(4, 1, 1))
710+
self.assertTrue(isinstance(str(sys), str))
711+
self.assertTrue(isinstance(sys._repr_latex_(), str))
712+
713+
# SISO, discrete time
714+
sys = sample_system(sys, 1)
715+
self.assertTrue(isinstance(str(sys), str))
716+
self.assertTrue(isinstance(sys._repr_latex_(), str))
717+
718+
# MIMO, continuous time
719+
sys = ss2tf(rss(4, 2, 3))
720+
self.assertTrue(isinstance(str(sys), str))
721+
self.assertTrue(isinstance(sys._repr_latex_(), str))
722+
723+
def test_size_mismatch(self):
724+
sys1 = ss2tf(rss(2, 2, 2))
725+
726+
# Different number of inputs
727+
sys2 = ss2tf(rss(3, 1, 2))
728+
self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2)
729+
730+
# Different number of outputs
731+
sys2 = ss2tf(rss(3, 2, 1))
732+
self.assertRaises(ValueError, TransferFunction.__add__, sys1, sys2)
733+
734+
# Inputs and outputs don't match
735+
self.assertRaises(ValueError, TransferFunction.__mul__, sys2, sys1)
736+
737+
# Feedback mismatch (MIMO not implemented)
738+
self.assertRaises(NotImplementedError,
739+
TransferFunction.feedback, sys2, sys1)
740+
622741

623742
def suite():
624743
return unittest.TestLoader().loadTestsFromTestCase(TestXferFcn)

control/xferfcn.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,10 @@ def __init__(self, *args):
121121
% type(args[0]))
122122
num = args[0].num
123123
den = args[0].den
124+
# TODO: not sure this can ever happen since dt is always present
124125
try:
125126
dt = args[0].dt
126-
except NameError:
127+
except NameError: # pragma: no coverage
127128
dt = None
128129
else:
129130
raise ValueError("Needs 1, 2 or 3 arguments; received %i."

0 commit comments

Comments
 (0)