test_control_plots.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. from math import isclose
  2. from sympy.core.numbers import I, all_close
  3. from sympy.core.symbol import Dummy
  4. from sympy.functions.elementary.complexes import (Abs, arg)
  5. from sympy.functions.elementary.exponential import log
  6. from sympy.functions.elementary.miscellaneous import sqrt
  7. from sympy.abc import s, p, a
  8. from sympy import pi
  9. from sympy.external import import_module
  10. from sympy.physics.control.control_plots import \
  11. (pole_zero_numerical_data, pole_zero_plot, step_response_numerical_data,
  12. step_response_plot, impulse_response_numerical_data,
  13. impulse_response_plot, ramp_response_numerical_data,
  14. ramp_response_plot, bode_magnitude_numerical_data,
  15. bode_phase_numerical_data, bode_plot, nyquist_plot_expr,
  16. nichols_plot_expr)
  17. from sympy.physics.control.lti import (TransferFunction,
  18. Series, Parallel, TransferFunctionMatrix)
  19. from sympy.testing.pytest import raises, skip
  20. matplotlib = import_module(
  21. 'matplotlib', import_kwargs={'fromlist': ['pyplot']},
  22. catch=(RuntimeError,))
  23. numpy = import_module('numpy')
  24. tf1 = TransferFunction(1, p**2 + 0.5*p + 2, p)
  25. tf2 = TransferFunction(p, 6*p**2 + 3*p + 1, p)
  26. tf3 = TransferFunction(p, p**3 - 1, p)
  27. tf4 = TransferFunction(10, p**3, p)
  28. tf5 = TransferFunction(5, s**2 + 2*s + 10, s)
  29. tf6 = TransferFunction(1, 1, s)
  30. tf7 = TransferFunction(4*s*3 + 9*s**2 + 0.1*s + 11, 8*s**6 + 9*s**4 + 11, s)
  31. tf8 = TransferFunction(5, s**2 + (2+I)*s + 10, s)
  32. ser1 = Series(tf4, TransferFunction(1, p - 5, p))
  33. ser2 = Series(tf3, TransferFunction(p, p + 2, p))
  34. par1 = Parallel(tf1, tf2)
  35. def _to_tuple(a, b):
  36. return tuple(a), tuple(b)
  37. def _trim_tuple(a, b):
  38. a, b = _to_tuple(a, b)
  39. return tuple(a[0: 2] + a[len(a)//2 : len(a)//2 + 1] + a[-2:]), \
  40. tuple(b[0: 2] + b[len(b)//2 : len(b)//2 + 1] + b[-2:])
  41. def y_coordinate_equality(plot_data_func, evalf_func, system):
  42. """Checks whether the y-coordinate value of the plotted
  43. data point is equal to the value of the function at a
  44. particular x."""
  45. x, y = plot_data_func(system)
  46. x, y = _trim_tuple(x, y)
  47. y_exp = tuple(evalf_func(system, x_i) for x_i in x)
  48. return all(Abs(y_exp_i - y_i) < 1e-8 for y_exp_i, y_i in zip(y_exp, y))
  49. def test_errors():
  50. if not matplotlib:
  51. skip("Matplotlib not the default backend")
  52. # Invalid `system` check
  53. tfm = TransferFunctionMatrix([[tf6, tf5], [tf5, tf6]])
  54. expr = 1/(s**2 - 1)
  55. raises(NotImplementedError, lambda: pole_zero_plot(tfm))
  56. raises(NotImplementedError, lambda: pole_zero_numerical_data(expr))
  57. raises(NotImplementedError, lambda: impulse_response_plot(expr))
  58. raises(NotImplementedError, lambda: impulse_response_numerical_data(tfm))
  59. raises(NotImplementedError, lambda: step_response_plot(tfm))
  60. raises(NotImplementedError, lambda: step_response_numerical_data(expr))
  61. raises(NotImplementedError, lambda: ramp_response_plot(expr))
  62. raises(NotImplementedError, lambda: ramp_response_numerical_data(tfm))
  63. raises(NotImplementedError, lambda: bode_plot(tfm))
  64. # More than 1 variables
  65. tf_a = TransferFunction(a, s + 1, s)
  66. raises(ValueError, lambda: pole_zero_plot(tf_a))
  67. raises(ValueError, lambda: pole_zero_numerical_data(tf_a))
  68. raises(ValueError, lambda: impulse_response_plot(tf_a))
  69. raises(ValueError, lambda: impulse_response_numerical_data(tf_a))
  70. raises(ValueError, lambda: step_response_plot(tf_a))
  71. raises(ValueError, lambda: step_response_numerical_data(tf_a))
  72. raises(ValueError, lambda: ramp_response_plot(tf_a))
  73. raises(ValueError, lambda: ramp_response_numerical_data(tf_a))
  74. raises(ValueError, lambda: bode_plot(tf_a))
  75. # lower_limit > 0 for response plots
  76. raises(ValueError, lambda: impulse_response_plot(tf1, lower_limit=-1))
  77. raises(ValueError, lambda: step_response_plot(tf1, lower_limit=-0.1))
  78. raises(ValueError, lambda: ramp_response_plot(tf1, lower_limit=-4/3))
  79. # slope in ramp_response_plot() is negative
  80. raises(ValueError, lambda: ramp_response_plot(tf1, slope=-0.1))
  81. # incorrect frequency or phase unit
  82. raises(ValueError, lambda: bode_plot(tf1,freq_unit = 'hz'))
  83. raises(ValueError, lambda: bode_plot(tf1,phase_unit = 'degree'))
  84. def test_pole_zero():
  85. def pz_tester(sys, expected_value):
  86. _z, _p = pole_zero_numerical_data(sys)
  87. z_check = all_close(_z, expected_value[0])
  88. p_check = all_close(_p, expected_value[1])
  89. return p_check and z_check
  90. exp1 = [[], [-0.24999999999999994-1.3919410907075054j, -0.24999999999999994+1.3919410907075054j]]
  91. exp2 = [[0.0], [-0.25-0.3227486121839514j, -0.25+0.3227486121839514j]]
  92. exp3 = [[0.0], [0.9999999999999998+0j, -0.5000000000000004-0.8660254037844395j,
  93. -0.5000000000000004+0.8660254037844395j]]
  94. exp4 = [[], [0.0, 0.0, 0.0, 5.0]]
  95. exp5 = [[-5.645751311064592, -0.5000000000000008, -0.3542486889354093],
  96. [-0.24999999999999986-0.322748612183951348j,
  97. -0.2499999999999998+0.32274861218395134j,
  98. -0.24999999999999986-1.3919410907075052j,
  99. -0.2499999999999998+1.3919410907075052j]]
  100. exp6 = [[], [-1.1641600331447917-3.545808351896439j,
  101. -0.8358399668552097+2.5458083518964383j]]
  102. assert pz_tester(tf1, exp1)
  103. assert pz_tester(tf2, exp2)
  104. assert pz_tester(tf3, exp3)
  105. assert pz_tester(ser1, exp4)
  106. assert pz_tester(par1, exp5)
  107. assert pz_tester(tf8, exp6)
  108. def test_bode():
  109. if not numpy:
  110. skip("NumPy is required for this test")
  111. def bode_phase_evalf(system, point):
  112. expr = system.to_expr()
  113. _w = Dummy("w", real=True)
  114. w_expr = expr.subs({system.var: I*_w})
  115. return arg(w_expr).subs({_w: point}).evalf()
  116. def bode_mag_evalf(system, point):
  117. expr = system.to_expr()
  118. _w = Dummy("w", real=True)
  119. w_expr = expr.subs({system.var: I*_w})
  120. return 20*log(Abs(w_expr), 10).subs({_w: point}).evalf()
  121. def test_bode_data(sys):
  122. return y_coordinate_equality(bode_magnitude_numerical_data, bode_mag_evalf, sys) \
  123. and y_coordinate_equality(bode_phase_numerical_data, bode_phase_evalf, sys)
  124. assert test_bode_data(tf1)
  125. assert test_bode_data(tf2)
  126. assert test_bode_data(tf3)
  127. assert test_bode_data(tf4)
  128. assert test_bode_data(tf5)
  129. def check_point_accuracy(a, b):
  130. return all(isclose(*_, rel_tol=1e-1, abs_tol=1e-6
  131. ) for _ in zip(a, b))
  132. def test_impulse_response():
  133. if not numpy:
  134. skip("NumPy is required for this test")
  135. def impulse_res_tester(sys, expected_value):
  136. x, y = _to_tuple(*impulse_response_numerical_data(sys,
  137. adaptive=False, n=10))
  138. x_check = check_point_accuracy(x, expected_value[0])
  139. y_check = check_point_accuracy(y, expected_value[1])
  140. return x_check and y_check
  141. exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  142. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  143. (0.0, 0.544019738507865, 0.01993849743234938, -0.31140243360893216, -0.022852779906491996, 0.1778306498155759,
  144. 0.01962941084328499, -0.1013115194573652, -0.014975541213105696, 0.0575789724730714))
  145. exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  146. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.1666666675, 0.08389223412935855,
  147. 0.02338051973475047, -0.014966807776379383, -0.034645954223054234, -0.040560075735512804,
  148. -0.037658628907103885, -0.030149507719590022, -0.021162090730736834, -0.012721292737437523))
  149. exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  150. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (4.369893391586999e-09, 1.1750333000630964,
  151. 3.2922404058312473, 9.432290008148343, 28.37098083007151, 86.18577464367974, 261.90356653762115,
  152. 795.6538758627842, 2416.9920942096983, 7342.159505206647))
  153. exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  154. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 6.17283950617284, 24.69135802469136,
  155. 55.555555555555564, 98.76543209876544, 154.320987654321, 222.22222222222226, 302.46913580246917,
  156. 395.0617283950618, 500.0))
  157. exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  158. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, -0.10455606138085417,
  159. 0.06757671513476461, -0.03234567568833768, 0.013582514927757873, -0.005273419510705473,
  160. 0.0019364083003354075, -0.000680070134067832, 0.00022969845960406913, -7.476094359583917e-05))
  161. exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  162. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  163. (-6.016699583000218e-09, 0.35039802056107394, 3.3728423827689884, 12.119846079276684,
  164. 25.86101014293389, 29.352480635282088, -30.49475907497664, -273.8717189554019, -863.2381702029659,
  165. -1747.0262164682233))
  166. exp7 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335,
  167. 4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779,
  168. 8.88888888888889, 10.0), (0.0, 18.934638095560974, 5346.93244680907, 1384609.8718249386,
  169. 358161126.65801865, 92645770015.70108, 23964739753087.42, 6198974342083139.0, 1.603492601616059e+18,
  170. 4.147764422869658e+20))
  171. assert impulse_res_tester(tf1, exp1)
  172. assert impulse_res_tester(tf2, exp2)
  173. assert impulse_res_tester(tf3, exp3)
  174. assert impulse_res_tester(tf4, exp4)
  175. assert impulse_res_tester(tf5, exp5)
  176. assert impulse_res_tester(tf7, exp6)
  177. assert impulse_res_tester(ser1, exp7)
  178. def test_step_response():
  179. if not numpy:
  180. skip("NumPy is required for this test")
  181. def step_res_tester(sys, expected_value):
  182. x, y = _to_tuple(*step_response_numerical_data(sys,
  183. adaptive=False, n=10))
  184. x_check = check_point_accuracy(x, expected_value[0])
  185. y_check = check_point_accuracy(y, expected_value[1])
  186. return x_check and y_check
  187. exp1 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  188. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  189. (-1.9193285738516863e-08, 0.42283495488246126, 0.7840485977945262, 0.5546841805655717,
  190. 0.33903033806932087, 0.4627251747410237, 0.5909907598988051, 0.5247213989553071,
  191. 0.4486997874319281, 0.4839358435839171))
  192. exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  193. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  194. (0.0, 0.13728409095645816, 0.19474559355325086, 0.1974909129243011, 0.16841657696573073,
  195. 0.12559777736159378, 0.08153828016664713, 0.04360471317348958, 0.015072994568868221,
  196. -0.003636420058445484))
  197. exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  198. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  199. (0.0, 0.6314542141914303, 2.9356520038101035, 9.37731009663807, 28.452300356688376,
  200. 86.25721933273988, 261.9236645044672, 795.6435410577224, 2416.9786984578764, 7342.154119725917))
  201. exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  202. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  203. (0.0, 2.286236899862826, 18.28989519890261, 61.72839629629631, 146.31916159122088, 285.7796124828532,
  204. 493.8271703703705, 784.1792566529494, 1170.553292729767, 1666.6667))
  205. exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  206. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  207. (-3.999999997894577e-09, 0.6720357068882895, 0.4429938256137113, 0.5182010838004518,
  208. 0.4944139147159695, 0.5016379853883338, 0.4995466896527733, 0.5001154784851325,
  209. 0.49997448824584123, 0.5000039745919259))
  210. exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  211. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  212. (-1.5433688493882158e-09, 0.3428705539937336, 1.1253619102202777, 3.1849962651016517,
  213. 9.47532757182671, 28.727231099148135, 87.29426924860557, 265.2138681048606, 805.6636260007757,
  214. 2447.387582370878))
  215. assert step_res_tester(tf1, exp1)
  216. assert step_res_tester(tf2, exp2)
  217. assert step_res_tester(tf3, exp3)
  218. assert step_res_tester(tf4, exp4)
  219. assert step_res_tester(tf5, exp5)
  220. assert step_res_tester(ser2, exp6)
  221. def test_ramp_response():
  222. if not numpy:
  223. skip("NumPy is required for this test")
  224. def ramp_res_tester(sys, num_points, expected_value, slope=1):
  225. x, y = _to_tuple(*ramp_response_numerical_data(sys,
  226. slope=slope, adaptive=False, n=num_points))
  227. x_check = check_point_accuracy(x, expected_value[0])
  228. y_check = check_point_accuracy(y, expected_value[1])
  229. return x_check and y_check
  230. exp1 = ((0.0, 2.0, 4.0, 6.0, 8.0, 10.0), (0.0, 0.7324667795033895, 1.9909720978650398,
  231. 2.7956587704217783, 3.9224897567931514, 4.85022655284895))
  232. exp2 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445,
  233. 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0),
  234. (2.4360213402019326e-08, 0.10175320182493253, 0.33057612497658406, 0.5967937263298935,
  235. 0.8431511866718248, 1.0398805391471613, 1.1776043125035738, 1.2600994825747305, 1.2981042689274653,
  236. 1.304684417610106))
  237. exp3 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  238. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (-3.9329040468771836e-08,
  239. 0.34686634635794555, 2.9998828170537903, 12.33303690737476, 40.993913948137795, 127.84145222317912,
  240. 391.41713691996, 1192.0006858708389, 3623.9808672503405, 11011.728034546572))
  241. exp4 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  242. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.9051973784484078, 30.483158055174524,
  243. 154.32098765432104, 487.7305288827924, 1190.7483615302544, 2469.1358024691367, 4574.3789056546275,
  244. 7803.688462124678, 12500.0))
  245. exp5 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  246. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 3.8844361856975635, 9.141792069209865,
  247. 14.096349157657231, 19.09783068994694, 24.10179770390321, 29.09907319114121, 34.10040420185154,
  248. 39.09983919254265, 44.10006013058409))
  249. exp6 = ((0.0, 1.1111111111111112, 2.2222222222222223, 3.3333333333333335, 4.444444444444445, 5.555555555555555,
  250. 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0), (0.0, 1.1111111111111112, 2.2222222222222223,
  251. 3.3333333333333335, 4.444444444444445, 5.555555555555555, 6.666666666666667, 7.777777777777779, 8.88888888888889, 10.0))
  252. assert ramp_res_tester(tf1, 6, exp1)
  253. assert ramp_res_tester(tf2, 10, exp2, 1.2)
  254. assert ramp_res_tester(tf3, 10, exp3, 1.5)
  255. assert ramp_res_tester(tf4, 10, exp4, 3)
  256. assert ramp_res_tester(tf5, 10, exp5, 9)
  257. assert ramp_res_tester(tf6, 10, exp6)
  258. def test_nyquist_plot_expr():
  259. r1, i1, w1 = nyquist_plot_expr(tf1)
  260. r2, i2, w2 = nyquist_plot_expr(tf2)
  261. r3, i3, w3 = nyquist_plot_expr(tf3)
  262. r4, i4, w4 = nyquist_plot_expr(tf4)
  263. assert r1 == (2 - w1**2)/(0.25*w1**2 + (2 - w1**2)**2)
  264. assert i1 == -0.5*w1/(0.25*w1**2 + (2 - w1**2)**2)
  265. assert r2 == 3*w2**2/(9*w2**2 + (1 - 6*w2**2)**2)
  266. assert i2 == w2*(1 - 6*w2**2)/(9*w2**2 + (1 - 6*w2**2)**2)
  267. assert r3 == -w3**4/(w3**6 + 1)
  268. assert i3 == -w3/(w3**6 + 1)
  269. assert r4 == 0
  270. assert i4 == 10/w4**3
  271. def test_nichols_expr():
  272. m1, p1, w1 = nichols_plot_expr(tf1)
  273. m2, p2, w2 = nichols_plot_expr(tf2)
  274. m3, p3, w3 = nichols_plot_expr(tf3)
  275. m4, p4, w4 = nichols_plot_expr(tf4)
  276. assert m1 == 20*log(1/sqrt(w1**4 - 3.75*w1**2 + 4))/log(10)
  277. assert p1 == 180*arg(1/(-w1**2 + 0.5*w1*I + 2))/pi
  278. assert m2 == 20*log(Abs(w2)/sqrt(36*w2**4 - 3*w2**2 + 1))/log(10)
  279. assert p2 == 180*arg(w2*I/(-6*w2**2 + 3*w2*I + 1))/pi
  280. assert m3 == 20*log(Abs(w3)/sqrt(w3**6 + 1))/log(10)
  281. assert p3 == 180*arg(-w3*I/(w3**3*I + 1))/pi
  282. assert m4 == 20*log(10/(w4**2*Abs(w4)))/log(10)
  283. assert p4 == 180*arg(I/w4**3)/pi