control_plots.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135
  1. from sympy.core.numbers import I, pi
  2. from sympy.functions.elementary.exponential import (exp, log)
  3. from sympy.polys.partfrac import apart
  4. from sympy.core.symbol import Dummy
  5. from sympy.external import import_module
  6. from sympy.functions import arg, Abs
  7. from sympy.integrals.laplace import _fast_inverse_laplace
  8. from sympy.physics.control.lti import SISOLinearTimeInvariant
  9. from sympy.plotting.series import LineOver1DRangeSeries
  10. from sympy.plotting.plot import plot_parametric
  11. from sympy.polys.domains import ZZ, QQ
  12. from sympy.polys.polytools import Poly
  13. from sympy.printing.latex import latex
  14. from sympy.geometry.polygon import deg
  15. __all__ = ['pole_zero_numerical_data', 'pole_zero_plot',
  16. 'step_response_numerical_data', 'step_response_plot',
  17. 'impulse_response_numerical_data', 'impulse_response_plot',
  18. 'ramp_response_numerical_data', 'ramp_response_plot',
  19. 'bode_magnitude_numerical_data', 'bode_phase_numerical_data',
  20. 'bode_magnitude_plot', 'bode_phase_plot', 'bode_plot',
  21. 'nyquist_plot_expr', 'nyquist_plot', 'nichols_plot_expr',
  22. 'nichols_plot']
  23. matplotlib = import_module(
  24. 'matplotlib', import_kwargs={'fromlist': ['pyplot']},
  25. catch=(RuntimeError,))
  26. if matplotlib:
  27. plt = matplotlib.pyplot
  28. def _check_system(system):
  29. """Function to check whether the dynamical system passed for plots is
  30. compatible or not."""
  31. if not isinstance(system, SISOLinearTimeInvariant):
  32. raise NotImplementedError("Only SISO LTI systems are currently supported.")
  33. sys = system.to_expr()
  34. len_free_symbols = len(sys.free_symbols)
  35. if len_free_symbols > 1:
  36. raise ValueError("Extra degree of freedom found. Make sure"
  37. " that there are no free symbols in the dynamical system other"
  38. " than the variable of Laplace transform.")
  39. if sys.has(exp):
  40. # Should test that exp is not part of a constant, in which case
  41. # no exception is required, compare exp(s) with s*exp(1)
  42. raise NotImplementedError("Time delay terms are not supported.")
  43. def _poly_roots(poly):
  44. """Function to get the roots of a polynomial."""
  45. def _eval(l):
  46. return [float(i) if i.is_real else complex(i) for i in l]
  47. if poly.domain in (QQ, ZZ):
  48. return _eval(poly.all_roots())
  49. # XXX: Use all_roots() for irrational coefficients when possible
  50. # See https://github.com/sympy/sympy/issues/22943
  51. return _eval(poly.nroots())
  52. def pole_zero_numerical_data(system):
  53. """
  54. Returns the numerical data of poles and zeros of the system.
  55. It is internally used by ``pole_zero_plot`` to get the data
  56. for plotting poles and zeros. Users can use this data to further
  57. analyse the dynamics of the system or plot using a different
  58. backend/plotting-module.
  59. Parameters
  60. ==========
  61. system : SISOLinearTimeInvariant
  62. The system for which the pole-zero data is to be computed.
  63. Returns
  64. =======
  65. tuple : (zeros, poles)
  66. zeros = Zeros of the system as a list of Python float/complex.
  67. poles = Poles of the system as a list of Python float/complex.
  68. Raises
  69. ======
  70. NotImplementedError
  71. When a SISO LTI system is not passed.
  72. When time delay terms are present in the system.
  73. ValueError
  74. When more than one free symbol is present in the system.
  75. The only variable in the transfer function should be
  76. the variable of the Laplace transform.
  77. Examples
  78. ========
  79. >>> from sympy.abc import s
  80. >>> from sympy.physics.control.lti import TransferFunction
  81. >>> from sympy.physics.control.control_plots import pole_zero_numerical_data
  82. >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
  83. >>> pole_zero_numerical_data(tf1)
  84. ([-1j, 1j], [-2.0, -1.0, (-0.5-0.8660254037844386j), (-0.5+0.8660254037844386j)])
  85. See Also
  86. ========
  87. pole_zero_plot
  88. """
  89. _check_system(system)
  90. system = system.doit() # Get the equivalent TransferFunction object.
  91. num_poly = Poly(system.num, system.var)
  92. den_poly = Poly(system.den, system.var)
  93. return _poly_roots(num_poly), _poly_roots(den_poly)
  94. def pole_zero_plot(system, pole_color='blue', pole_markersize=10,
  95. zero_color='orange', zero_markersize=7, grid=True, show_axes=True,
  96. show=True, **kwargs):
  97. r"""
  98. Returns the Pole-Zero plot (also known as PZ Plot or PZ Map) of a system.
  99. A Pole-Zero plot is a graphical representation of a system's poles and
  100. zeros. It is plotted on a complex plane, with circular markers representing
  101. the system's zeros and 'x' shaped markers representing the system's poles.
  102. Parameters
  103. ==========
  104. system : SISOLinearTimeInvariant type systems
  105. The system for which the pole-zero plot is to be computed.
  106. pole_color : str, tuple, optional
  107. The color of the pole points on the plot. Default color
  108. is blue. The color can be provided as a matplotlib color string,
  109. or a 3-tuple of floats each in the 0-1 range.
  110. pole_markersize : Number, optional
  111. The size of the markers used to mark the poles in the plot.
  112. Default pole markersize is 10.
  113. zero_color : str, tuple, optional
  114. The color of the zero points on the plot. Default color
  115. is orange. The color can be provided as a matplotlib color string,
  116. or a 3-tuple of floats each in the 0-1 range.
  117. zero_markersize : Number, optional
  118. The size of the markers used to mark the zeros in the plot.
  119. Default zero markersize is 7.
  120. grid : boolean, optional
  121. If ``True``, the plot will have a grid. Defaults to True.
  122. show_axes : boolean, optional
  123. If ``True``, the coordinate axes will be shown. Defaults to False.
  124. show : boolean, optional
  125. If ``True``, the plot will be displayed otherwise
  126. the equivalent matplotlib ``plot`` object will be returned.
  127. Defaults to True.
  128. Examples
  129. ========
  130. .. plot::
  131. :context: close-figs
  132. :format: doctest
  133. :include-source: True
  134. >>> from sympy.abc import s
  135. >>> from sympy.physics.control.lti import TransferFunction
  136. >>> from sympy.physics.control.control_plots import pole_zero_plot
  137. >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
  138. >>> pole_zero_plot(tf1) # doctest: +SKIP
  139. See Also
  140. ========
  141. pole_zero_numerical_data
  142. References
  143. ==========
  144. .. [1] https://en.wikipedia.org/wiki/Pole%E2%80%93zero_plot
  145. """
  146. zeros, poles = pole_zero_numerical_data(system)
  147. zero_real = [i.real for i in zeros]
  148. zero_imag = [i.imag for i in zeros]
  149. pole_real = [i.real for i in poles]
  150. pole_imag = [i.imag for i in poles]
  151. plt.plot(pole_real, pole_imag, 'x', mfc='none',
  152. markersize=pole_markersize, color=pole_color)
  153. plt.plot(zero_real, zero_imag, 'o', markersize=zero_markersize,
  154. color=zero_color)
  155. plt.xlabel('Real Axis')
  156. plt.ylabel('Imaginary Axis')
  157. plt.title(f'Poles and Zeros of ${latex(system)}$', pad=20)
  158. if grid:
  159. plt.grid()
  160. if show_axes:
  161. plt.axhline(0, color='black')
  162. plt.axvline(0, color='black')
  163. if show:
  164. plt.show()
  165. return
  166. return plt
  167. def step_response_numerical_data(system, prec=8, lower_limit=0,
  168. upper_limit=10, **kwargs):
  169. """
  170. Returns the numerical values of the points in the step response plot
  171. of a SISO continuous-time system. By default, adaptive sampling
  172. is used. If the user wants to instead get an uniformly
  173. sampled response, then ``adaptive`` kwarg should be passed ``False``
  174. and ``n`` must be passed as additional kwargs.
  175. Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
  176. for more details.
  177. Parameters
  178. ==========
  179. system : SISOLinearTimeInvariant
  180. The system for which the unit step response data is to be computed.
  181. prec : int, optional
  182. The decimal point precision for the point coordinate values.
  183. Defaults to 8.
  184. lower_limit : Number, optional
  185. The lower limit of the plot range. Defaults to 0.
  186. upper_limit : Number, optional
  187. The upper limit of the plot range. Defaults to 10.
  188. kwargs :
  189. Additional keyword arguments are passed to the underlying
  190. :class:`sympy.plotting.series.LineOver1DRangeSeries` class.
  191. Returns
  192. =======
  193. tuple : (x, y)
  194. x = Time-axis values of the points in the step response. NumPy array.
  195. y = Amplitude-axis values of the points in the step response. NumPy array.
  196. Raises
  197. ======
  198. NotImplementedError
  199. When a SISO LTI system is not passed.
  200. When time delay terms are present in the system.
  201. ValueError
  202. When more than one free symbol is present in the system.
  203. The only variable in the transfer function should be
  204. the variable of the Laplace transform.
  205. When ``lower_limit`` parameter is less than 0.
  206. Examples
  207. ========
  208. >>> from sympy.abc import s
  209. >>> from sympy.physics.control.lti import TransferFunction
  210. >>> from sympy.physics.control.control_plots import step_response_numerical_data
  211. >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
  212. >>> step_response_numerical_data(tf1) # doctest: +SKIP
  213. ([0.0, 0.025413462339411542, 0.0484508722725343, ... , 9.670250533855183, 9.844291913708725, 10.0],
  214. [0.0, 0.023844582399907256, 0.042894276802320226, ..., 6.828770759094287e-12, 6.456457160755703e-12])
  215. See Also
  216. ========
  217. step_response_plot
  218. """
  219. if lower_limit < 0:
  220. raise ValueError("Lower limit of time must be greater "
  221. "than or equal to zero.")
  222. _check_system(system)
  223. _x = Dummy("x")
  224. expr = system.to_expr()/(system.var)
  225. expr = apart(expr, system.var, full=True)
  226. _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
  227. return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
  228. **kwargs).get_points()
  229. def step_response_plot(system, color='b', prec=8, lower_limit=0,
  230. upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
  231. r"""
  232. Returns the unit step response of a continuous-time system. It is
  233. the response of the system when the input signal is a step function.
  234. Parameters
  235. ==========
  236. system : SISOLinearTimeInvariant type
  237. The LTI SISO system for which the Step Response is to be computed.
  238. color : str, tuple, optional
  239. The color of the line. Default is Blue.
  240. show : boolean, optional
  241. If ``True``, the plot will be displayed otherwise
  242. the equivalent matplotlib ``plot`` object will be returned.
  243. Defaults to True.
  244. lower_limit : Number, optional
  245. The lower limit of the plot range. Defaults to 0.
  246. upper_limit : Number, optional
  247. The upper limit of the plot range. Defaults to 10.
  248. prec : int, optional
  249. The decimal point precision for the point coordinate values.
  250. Defaults to 8.
  251. show_axes : boolean, optional
  252. If ``True``, the coordinate axes will be shown. Defaults to False.
  253. grid : boolean, optional
  254. If ``True``, the plot will have a grid. Defaults to True.
  255. Examples
  256. ========
  257. .. plot::
  258. :context: close-figs
  259. :format: doctest
  260. :include-source: True
  261. >>> from sympy.abc import s
  262. >>> from sympy.physics.control.lti import TransferFunction
  263. >>> from sympy.physics.control.control_plots import step_response_plot
  264. >>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s)
  265. >>> step_response_plot(tf1) # doctest: +SKIP
  266. See Also
  267. ========
  268. impulse_response_plot, ramp_response_plot
  269. References
  270. ==========
  271. .. [1] https://www.mathworks.com/help/control/ref/lti.step.html
  272. """
  273. x, y = step_response_numerical_data(system, prec=prec,
  274. lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
  275. plt.plot(x, y, color=color)
  276. plt.xlabel('Time (s)')
  277. plt.ylabel('Amplitude')
  278. plt.title(f'Unit Step Response of ${latex(system)}$', pad=20)
  279. if grid:
  280. plt.grid()
  281. if show_axes:
  282. plt.axhline(0, color='black')
  283. plt.axvline(0, color='black')
  284. if show:
  285. plt.show()
  286. return
  287. return plt
  288. def impulse_response_numerical_data(system, prec=8, lower_limit=0,
  289. upper_limit=10, **kwargs):
  290. """
  291. Returns the numerical values of the points in the impulse response plot
  292. of a SISO continuous-time system. By default, adaptive sampling
  293. is used. If the user wants to instead get an uniformly
  294. sampled response, then ``adaptive`` kwarg should be passed ``False``
  295. and ``n`` must be passed as additional kwargs.
  296. Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
  297. for more details.
  298. Parameters
  299. ==========
  300. system : SISOLinearTimeInvariant
  301. The system for which the impulse response data is to be computed.
  302. prec : int, optional
  303. The decimal point precision for the point coordinate values.
  304. Defaults to 8.
  305. lower_limit : Number, optional
  306. The lower limit of the plot range. Defaults to 0.
  307. upper_limit : Number, optional
  308. The upper limit of the plot range. Defaults to 10.
  309. kwargs :
  310. Additional keyword arguments are passed to the underlying
  311. :class:`sympy.plotting.series.LineOver1DRangeSeries` class.
  312. Returns
  313. =======
  314. tuple : (x, y)
  315. x = Time-axis values of the points in the impulse response. NumPy array.
  316. y = Amplitude-axis values of the points in the impulse response. NumPy array.
  317. Raises
  318. ======
  319. NotImplementedError
  320. When a SISO LTI system is not passed.
  321. When time delay terms are present in the system.
  322. ValueError
  323. When more than one free symbol is present in the system.
  324. The only variable in the transfer function should be
  325. the variable of the Laplace transform.
  326. When ``lower_limit`` parameter is less than 0.
  327. Examples
  328. ========
  329. >>> from sympy.abc import s
  330. >>> from sympy.physics.control.lti import TransferFunction
  331. >>> from sympy.physics.control.control_plots import impulse_response_numerical_data
  332. >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
  333. >>> impulse_response_numerical_data(tf1) # doctest: +SKIP
  334. ([0.0, 0.06616480200395854,... , 9.854500743565858, 10.0],
  335. [0.9999999799999999, 0.7042848373025861,...,7.170748906965121e-13, -5.1901263495547205e-12])
  336. See Also
  337. ========
  338. impulse_response_plot
  339. """
  340. if lower_limit < 0:
  341. raise ValueError("Lower limit of time must be greater "
  342. "than or equal to zero.")
  343. _check_system(system)
  344. _x = Dummy("x")
  345. expr = system.to_expr()
  346. expr = apart(expr, system.var, full=True)
  347. _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
  348. return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
  349. **kwargs).get_points()
  350. def impulse_response_plot(system, color='b', prec=8, lower_limit=0,
  351. upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
  352. r"""
  353. Returns the unit impulse response (Input is the Dirac-Delta Function) of a
  354. continuous-time system.
  355. Parameters
  356. ==========
  357. system : SISOLinearTimeInvariant type
  358. The LTI SISO system for which the Impulse Response is to be computed.
  359. color : str, tuple, optional
  360. The color of the line. Default is Blue.
  361. show : boolean, optional
  362. If ``True``, the plot will be displayed otherwise
  363. the equivalent matplotlib ``plot`` object will be returned.
  364. Defaults to True.
  365. lower_limit : Number, optional
  366. The lower limit of the plot range. Defaults to 0.
  367. upper_limit : Number, optional
  368. The upper limit of the plot range. Defaults to 10.
  369. prec : int, optional
  370. The decimal point precision for the point coordinate values.
  371. Defaults to 8.
  372. show_axes : boolean, optional
  373. If ``True``, the coordinate axes will be shown. Defaults to False.
  374. grid : boolean, optional
  375. If ``True``, the plot will have a grid. Defaults to True.
  376. Examples
  377. ========
  378. .. plot::
  379. :context: close-figs
  380. :format: doctest
  381. :include-source: True
  382. >>> from sympy.abc import s
  383. >>> from sympy.physics.control.lti import TransferFunction
  384. >>> from sympy.physics.control.control_plots import impulse_response_plot
  385. >>> tf1 = TransferFunction(8*s**2 + 18*s + 32, s**3 + 6*s**2 + 14*s + 24, s)
  386. >>> impulse_response_plot(tf1) # doctest: +SKIP
  387. See Also
  388. ========
  389. step_response_plot, ramp_response_plot
  390. References
  391. ==========
  392. .. [1] https://www.mathworks.com/help/control/ref/dynamicsystem.impulse.html
  393. """
  394. x, y = impulse_response_numerical_data(system, prec=prec,
  395. lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
  396. plt.plot(x, y, color=color)
  397. plt.xlabel('Time (s)')
  398. plt.ylabel('Amplitude')
  399. plt.title(f'Impulse Response of ${latex(system)}$', pad=20)
  400. if grid:
  401. plt.grid()
  402. if show_axes:
  403. plt.axhline(0, color='black')
  404. plt.axvline(0, color='black')
  405. if show:
  406. plt.show()
  407. return
  408. return plt
  409. def ramp_response_numerical_data(system, slope=1, prec=8,
  410. lower_limit=0, upper_limit=10, **kwargs):
  411. """
  412. Returns the numerical values of the points in the ramp response plot
  413. of a SISO continuous-time system. By default, adaptive sampling
  414. is used. If the user wants to instead get an uniformly
  415. sampled response, then ``adaptive`` kwarg should be passed ``False``
  416. and ``n`` must be passed as additional kwargs.
  417. Refer to the parameters of class :class:`sympy.plotting.series.LineOver1DRangeSeries`
  418. for more details.
  419. Parameters
  420. ==========
  421. system : SISOLinearTimeInvariant
  422. The system for which the ramp response data is to be computed.
  423. slope : Number, optional
  424. The slope of the input ramp function. Defaults to 1.
  425. prec : int, optional
  426. The decimal point precision for the point coordinate values.
  427. Defaults to 8.
  428. lower_limit : Number, optional
  429. The lower limit of the plot range. Defaults to 0.
  430. upper_limit : Number, optional
  431. The upper limit of the plot range. Defaults to 10.
  432. kwargs :
  433. Additional keyword arguments are passed to the underlying
  434. :class:`sympy.plotting.series.LineOver1DRangeSeries` class.
  435. Returns
  436. =======
  437. tuple : (x, y)
  438. x = Time-axis values of the points in the ramp response plot. NumPy array.
  439. y = Amplitude-axis values of the points in the ramp response plot. NumPy array.
  440. Raises
  441. ======
  442. NotImplementedError
  443. When a SISO LTI system is not passed.
  444. When time delay terms are present in the system.
  445. ValueError
  446. When more than one free symbol is present in the system.
  447. The only variable in the transfer function should be
  448. the variable of the Laplace transform.
  449. When ``lower_limit`` parameter is less than 0.
  450. When ``slope`` is negative.
  451. Examples
  452. ========
  453. >>> from sympy.abc import s
  454. >>> from sympy.physics.control.lti import TransferFunction
  455. >>> from sympy.physics.control.control_plots import ramp_response_numerical_data
  456. >>> tf1 = TransferFunction(s, s**2 + 5*s + 8, s)
  457. >>> ramp_response_numerical_data(tf1) # doctest: +SKIP
  458. (([0.0, 0.12166980856813935,..., 9.861246379582118, 10.0],
  459. [1.4504508011325967e-09, 0.006046440489058766,..., 0.12499999999568202, 0.12499999999661349]))
  460. See Also
  461. ========
  462. ramp_response_plot
  463. """
  464. if slope < 0:
  465. raise ValueError("Slope must be greater than or equal"
  466. " to zero.")
  467. if lower_limit < 0:
  468. raise ValueError("Lower limit of time must be greater "
  469. "than or equal to zero.")
  470. _check_system(system)
  471. _x = Dummy("x")
  472. expr = (slope*system.to_expr())/((system.var)**2)
  473. expr = apart(expr, system.var, full=True)
  474. _y = _fast_inverse_laplace(expr, system.var, _x).evalf(prec)
  475. return LineOver1DRangeSeries(_y, (_x, lower_limit, upper_limit),
  476. **kwargs).get_points()
  477. def ramp_response_plot(system, slope=1, color='b', prec=8, lower_limit=0,
  478. upper_limit=10, show_axes=False, grid=True, show=True, **kwargs):
  479. r"""
  480. Returns the ramp response of a continuous-time system.
  481. Ramp function is defined as the straight line
  482. passing through origin ($f(x) = mx$). The slope of
  483. the ramp function can be varied by the user and
  484. the default value is 1.
  485. Parameters
  486. ==========
  487. system : SISOLinearTimeInvariant type
  488. The LTI SISO system for which the Ramp Response is to be computed.
  489. slope : Number, optional
  490. The slope of the input ramp function. Defaults to 1.
  491. color : str, tuple, optional
  492. The color of the line. Default is Blue.
  493. show : boolean, optional
  494. If ``True``, the plot will be displayed otherwise
  495. the equivalent matplotlib ``plot`` object will be returned.
  496. Defaults to True.
  497. lower_limit : Number, optional
  498. The lower limit of the plot range. Defaults to 0.
  499. upper_limit : Number, optional
  500. The upper limit of the plot range. Defaults to 10.
  501. prec : int, optional
  502. The decimal point precision for the point coordinate values.
  503. Defaults to 8.
  504. show_axes : boolean, optional
  505. If ``True``, the coordinate axes will be shown. Defaults to False.
  506. grid : boolean, optional
  507. If ``True``, the plot will have a grid. Defaults to True.
  508. Examples
  509. ========
  510. .. plot::
  511. :context: close-figs
  512. :format: doctest
  513. :include-source: True
  514. >>> from sympy.abc import s
  515. >>> from sympy.physics.control.lti import TransferFunction
  516. >>> from sympy.physics.control.control_plots import ramp_response_plot
  517. >>> tf1 = TransferFunction(s, (s+4)*(s+8), s)
  518. >>> ramp_response_plot(tf1, upper_limit=2) # doctest: +SKIP
  519. See Also
  520. ========
  521. step_response_plot, impulse_response_plot
  522. References
  523. ==========
  524. .. [1] https://en.wikipedia.org/wiki/Ramp_function
  525. """
  526. x, y = ramp_response_numerical_data(system, slope=slope, prec=prec,
  527. lower_limit=lower_limit, upper_limit=upper_limit, **kwargs)
  528. plt.plot(x, y, color=color)
  529. plt.xlabel('Time (s)')
  530. plt.ylabel('Amplitude')
  531. plt.title(f'Ramp Response of ${latex(system)}$ [Slope = {slope}]', pad=20)
  532. if grid:
  533. plt.grid()
  534. if show_axes:
  535. plt.axhline(0, color='black')
  536. plt.axvline(0, color='black')
  537. if show:
  538. plt.show()
  539. return
  540. return plt
  541. def bode_magnitude_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', **kwargs):
  542. """
  543. Returns the numerical data of the Bode magnitude plot of the system.
  544. It is internally used by ``bode_magnitude_plot`` to get the data
  545. for plotting Bode magnitude plot. Users can use this data to further
  546. analyse the dynamics of the system or plot using a different
  547. backend/plotting-module.
  548. Parameters
  549. ==========
  550. system : SISOLinearTimeInvariant
  551. The system for which the data is to be computed.
  552. initial_exp : Number, optional
  553. The initial exponent of 10 of the semilog plot. Defaults to -5.
  554. final_exp : Number, optional
  555. The final exponent of 10 of the semilog plot. Defaults to 5.
  556. freq_unit : string, optional
  557. User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units.
  558. Returns
  559. =======
  560. tuple : (x, y)
  561. x = x-axis values of the Bode magnitude plot.
  562. y = y-axis values of the Bode magnitude plot.
  563. Raises
  564. ======
  565. NotImplementedError
  566. When a SISO LTI system is not passed.
  567. When time delay terms are present in the system.
  568. ValueError
  569. When more than one free symbol is present in the system.
  570. The only variable in the transfer function should be
  571. the variable of the Laplace transform.
  572. When incorrect frequency units are given as input.
  573. Examples
  574. ========
  575. >>> from sympy.abc import s
  576. >>> from sympy.physics.control.lti import TransferFunction
  577. >>> from sympy.physics.control.control_plots import bode_magnitude_numerical_data
  578. >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
  579. >>> bode_magnitude_numerical_data(tf1) # doctest: +SKIP
  580. ([1e-05, 1.5148378120533502e-05,..., 68437.36188804005, 100000.0],
  581. [-6.020599914256786, -6.0205999155219505,..., -193.4117304087953, -200.00000000260573])
  582. See Also
  583. ========
  584. bode_magnitude_plot, bode_phase_numerical_data
  585. """
  586. _check_system(system)
  587. expr = system.to_expr()
  588. freq_units = ('rad/sec', 'Hz')
  589. if freq_unit not in freq_units:
  590. raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.')
  591. _w = Dummy("w", real=True)
  592. if freq_unit == 'Hz':
  593. repl = I*_w*2*pi
  594. else:
  595. repl = I*_w
  596. w_expr = expr.subs({system.var: repl})
  597. mag = 20*log(Abs(w_expr), 10)
  598. x, y = LineOver1DRangeSeries(mag,
  599. (_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points()
  600. return x, y
  601. def bode_magnitude_plot(system, initial_exp=-5, final_exp=5,
  602. color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', **kwargs):
  603. r"""
  604. Returns the Bode magnitude plot of a continuous-time system.
  605. See ``bode_plot`` for all the parameters.
  606. """
  607. x, y = bode_magnitude_numerical_data(system, initial_exp=initial_exp,
  608. final_exp=final_exp, freq_unit=freq_unit)
  609. plt.plot(x, y, color=color, **kwargs)
  610. plt.xscale('log')
  611. plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit)
  612. plt.ylabel('Magnitude (dB)')
  613. plt.title(f'Bode Plot (Magnitude) of ${latex(system)}$', pad=20)
  614. if grid:
  615. plt.grid(True)
  616. if show_axes:
  617. plt.axhline(0, color='black')
  618. plt.axvline(0, color='black')
  619. if show:
  620. plt.show()
  621. return
  622. return plt
  623. def bode_phase_numerical_data(system, initial_exp=-5, final_exp=5, freq_unit='rad/sec', phase_unit='rad', phase_unwrap = True, **kwargs):
  624. """
  625. Returns the numerical data of the Bode phase plot of the system.
  626. It is internally used by ``bode_phase_plot`` to get the data
  627. for plotting Bode phase plot. Users can use this data to further
  628. analyse the dynamics of the system or plot using a different
  629. backend/plotting-module.
  630. Parameters
  631. ==========
  632. system : SISOLinearTimeInvariant
  633. The system for which the Bode phase plot data is to be computed.
  634. initial_exp : Number, optional
  635. The initial exponent of 10 of the semilog plot. Defaults to -5.
  636. final_exp : Number, optional
  637. The final exponent of 10 of the semilog plot. Defaults to 5.
  638. freq_unit : string, optional
  639. User can choose between ``'rad/sec'`` (radians/second) and '``'Hz'`` (Hertz) as frequency units.
  640. phase_unit : string, optional
  641. User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units.
  642. phase_unwrap : bool, optional
  643. Set to ``True`` by default.
  644. Returns
  645. =======
  646. tuple : (x, y)
  647. x = x-axis values of the Bode phase plot.
  648. y = y-axis values of the Bode phase plot.
  649. Raises
  650. ======
  651. NotImplementedError
  652. When a SISO LTI system is not passed.
  653. When time delay terms are present in the system.
  654. ValueError
  655. When more than one free symbol is present in the system.
  656. The only variable in the transfer function should be
  657. the variable of the Laplace transform.
  658. When incorrect frequency or phase units are given as input.
  659. Examples
  660. ========
  661. >>> from sympy.abc import s
  662. >>> from sympy.physics.control.lti import TransferFunction
  663. >>> from sympy.physics.control.control_plots import bode_phase_numerical_data
  664. >>> tf1 = TransferFunction(s**2 + 1, s**4 + 4*s**3 + 6*s**2 + 5*s + 2, s)
  665. >>> bode_phase_numerical_data(tf1) # doctest: +SKIP
  666. ([1e-05, 1.4472354033813751e-05, 2.035581932165858e-05,..., 47577.3248186011, 67884.09326036123, 100000.0],
  667. [-2.5000000000291665e-05, -3.6180885085e-05, -5.08895483066e-05,...,-3.1415085799262523, -3.14155265358979])
  668. See Also
  669. ========
  670. bode_magnitude_plot, bode_phase_numerical_data
  671. """
  672. _check_system(system)
  673. expr = system.to_expr()
  674. freq_units = ('rad/sec', 'Hz')
  675. phase_units = ('rad', 'deg')
  676. if freq_unit not in freq_units:
  677. raise ValueError('Only "rad/sec" and "Hz" are accepted frequency units.')
  678. if phase_unit not in phase_units:
  679. raise ValueError('Only "rad" and "deg" are accepted phase units.')
  680. _w = Dummy("w", real=True)
  681. if freq_unit == 'Hz':
  682. repl = I*_w*2*pi
  683. else:
  684. repl = I*_w
  685. w_expr = expr.subs({system.var: repl})
  686. if phase_unit == 'deg':
  687. phase = arg(w_expr)*180/pi
  688. else:
  689. phase = arg(w_expr)
  690. x, y = LineOver1DRangeSeries(phase,
  691. (_w, 10**initial_exp, 10**final_exp), xscale='log', **kwargs).get_points()
  692. half = None
  693. if phase_unwrap:
  694. if(phase_unit == 'rad'):
  695. half = pi
  696. elif(phase_unit == 'deg'):
  697. half = 180
  698. if half:
  699. unit = 2*half
  700. for i in range(1, len(y)):
  701. diff = y[i] - y[i - 1]
  702. if diff > half: # Jump from -half to half
  703. y[i] = (y[i] - unit)
  704. elif diff < -half: # Jump from half to -half
  705. y[i] = (y[i] + unit)
  706. return x, y
  707. def bode_phase_plot(system, initial_exp=-5, final_exp=5,
  708. color='b', show_axes=False, grid=True, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs):
  709. r"""
  710. Returns the Bode phase plot of a continuous-time system.
  711. See ``bode_plot`` for all the parameters.
  712. """
  713. x, y = bode_phase_numerical_data(system, initial_exp=initial_exp,
  714. final_exp=final_exp, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap)
  715. plt.plot(x, y, color=color, **kwargs)
  716. plt.xscale('log')
  717. plt.xlabel('Frequency (%s) [Log Scale]' % freq_unit)
  718. plt.ylabel('Phase (%s)' % phase_unit)
  719. plt.title(f'Bode Plot (Phase) of ${latex(system)}$', pad=20)
  720. if grid:
  721. plt.grid(True)
  722. if show_axes:
  723. plt.axhline(0, color='black')
  724. plt.axvline(0, color='black')
  725. if show:
  726. plt.show()
  727. return
  728. return plt
  729. def bode_plot(system, initial_exp=-5, final_exp=5,
  730. grid=True, show_axes=False, show=True, freq_unit='rad/sec', phase_unit='rad', phase_unwrap=True, **kwargs):
  731. r"""
  732. Returns the Bode phase and magnitude plots of a continuous-time system.
  733. Parameters
  734. ==========
  735. system : SISOLinearTimeInvariant type
  736. The LTI SISO system for which the Bode Plot is to be computed.
  737. initial_exp : Number, optional
  738. The initial exponent of 10 of the semilog plot. Defaults to -5.
  739. final_exp : Number, optional
  740. The final exponent of 10 of the semilog plot. Defaults to 5.
  741. show : boolean, optional
  742. If ``True``, the plot will be displayed otherwise
  743. the equivalent matplotlib ``plot`` object will be returned.
  744. Defaults to True.
  745. prec : int, optional
  746. The decimal point precision for the point coordinate values.
  747. Defaults to 8.
  748. grid : boolean, optional
  749. If ``True``, the plot will have a grid. Defaults to True.
  750. show_axes : boolean, optional
  751. If ``True``, the coordinate axes will be shown. Defaults to False.
  752. freq_unit : string, optional
  753. User can choose between ``'rad/sec'`` (radians/second) and ``'Hz'`` (Hertz) as frequency units.
  754. phase_unit : string, optional
  755. User can choose between ``'rad'`` (radians) and ``'deg'`` (degree) as phase units.
  756. Examples
  757. ========
  758. .. plot::
  759. :context: close-figs
  760. :format: doctest
  761. :include-source: True
  762. >>> from sympy.abc import s
  763. >>> from sympy.physics.control.lti import TransferFunction
  764. >>> from sympy.physics.control.control_plots import bode_plot
  765. >>> tf1 = TransferFunction(1*s**2 + 0.1*s + 7.5, 1*s**4 + 0.12*s**3 + 9*s**2, s)
  766. >>> bode_plot(tf1, initial_exp=0.2, final_exp=0.7) # doctest: +SKIP
  767. See Also
  768. ========
  769. bode_magnitude_plot, bode_phase_plot
  770. """
  771. plt.subplot(211)
  772. mag = bode_magnitude_plot(system, initial_exp=initial_exp, final_exp=final_exp,
  773. show=False, grid=grid, show_axes=show_axes,
  774. freq_unit=freq_unit, **kwargs)
  775. mag.title(f'Bode Plot of ${latex(system)}$', pad=20)
  776. mag.xlabel(None)
  777. plt.subplot(212)
  778. bode_phase_plot(system, initial_exp=initial_exp, final_exp=final_exp,
  779. show=False, grid=grid, show_axes=show_axes, freq_unit=freq_unit, phase_unit=phase_unit, phase_unwrap=phase_unwrap, **kwargs).title(None)
  780. if show:
  781. plt.show()
  782. return
  783. return plt
  784. def nyquist_plot_expr(system):
  785. """Function to get the expression for Nyquist plot."""
  786. s = system.var
  787. w = Dummy('w', real=True)
  788. repl = I * w
  789. expr = system.to_expr()
  790. w_expr = expr.subs({s: repl})
  791. w_expr = w_expr.as_real_imag()
  792. real_expr = w_expr[0]
  793. imag_expr = w_expr[1]
  794. return real_expr, imag_expr, w
  795. def nichols_plot_expr(system):
  796. """Function to get the expression for Nichols plot."""
  797. s = system.var
  798. w = Dummy('w', real=True)
  799. sys_expr = system.to_expr()
  800. H_jw = sys_expr.subs(s, I*w)
  801. mag_expr = Abs(H_jw)
  802. mag_dB_expr = 20*log(mag_expr, 10)
  803. phase_expr = arg(H_jw)
  804. phase_deg_expr = deg(phase_expr)
  805. return mag_dB_expr, phase_deg_expr, w
  806. def nyquist_plot(system, initial_omega=0.01, final_omega=100, show=True,
  807. color='b', **kwargs):
  808. r"""
  809. Generates the Nyquist plot for a continuous-time system.
  810. Parameters
  811. ==========
  812. system : SISOLinearTimeInvariant
  813. The LTI SISO system for which the Nyquist plot is to be generated.
  814. initial_omega : float, optional
  815. The starting frequency value. Defaults to 0.01.
  816. final_omega : float, optional
  817. The ending frequency value. Defaults to 100.
  818. show : bool, optional
  819. If True, the plot is displayed. Default is True.
  820. color : str, optional
  821. The color of the Nyquist plot. Default is 'b' (blue).
  822. grid : bool, optional
  823. If True, grid lines are displayed. Default is False.
  824. **kwargs
  825. Additional keyword arguments for customization.
  826. Examples
  827. ========
  828. .. plot::
  829. :context: close-figs
  830. :format: doctest
  831. :include-source: True
  832. >>> from sympy.abc import s
  833. >>> from sympy.physics.control.lti import TransferFunction
  834. >>> from sympy.physics.control.control_plots import nyquist_plot
  835. >>> tf1 = TransferFunction(2*s**2 + 5*s + 1, s**2 + 2*s + 3, s)
  836. >>> nyquist_plot(tf1) # doctest: +SKIP
  837. See Also
  838. ========
  839. nichols_plot, bode_plot
  840. """
  841. _check_system(system)
  842. real_expr, imag_expr, w = nyquist_plot_expr(system)
  843. w_values = [(w, initial_omega, final_omega)]
  844. p = plot_parametric(
  845. (real_expr, imag_expr), # The curve
  846. (real_expr, -imag_expr), # Its mirror image
  847. *w_values,
  848. show=False,
  849. line_color=color,
  850. adaptive=True,
  851. title=f'Nyquist Plot of ${latex(system)}$',
  852. xlabel='Real Axis',
  853. ylabel='Imaginary Axis',
  854. size=(6, 5),
  855. kwargs=kwargs)
  856. if show:
  857. p.show()
  858. return
  859. return p
  860. def nichols_plot(system, initial_omega=0.01, final_omega=100, show=True, color='b', **kwargs):
  861. r"""
  862. Generates the Nichols plot for a LTI system.
  863. Parameters
  864. ==========
  865. system : SISOLinearTimeInvariant
  866. The LTI SISO system for which the Nyquist plot is to be generated.
  867. initial_omega : float, optional
  868. The starting frequency value. Defaults to 0.01.
  869. final_omega : float, optional
  870. The ending frequency value. Defaults to 100.
  871. show : bool, optional
  872. If True, the plot is displayed. Default is True.
  873. color : str, optional
  874. The color of the Nyquist plot. Default is 'b' (blue).
  875. grid : bool, optional
  876. If True, grid lines are displayed. Default is False.
  877. **kwargs
  878. Additional keyword arguments for customization.
  879. Examples
  880. ========
  881. .. plot::
  882. :context: close-figs
  883. :format: doctest
  884. :include-source: True
  885. >>> from sympy.abc import s
  886. >>> from sympy.physics.control.lti import TransferFunction
  887. >>> from sympy.physics.control.control_plots import nichols_plot
  888. >>> tf1 = TransferFunction(1.5, s**2+14*s+40.02, s)
  889. >>> nichols_plot(tf1) # doctest: +SKIP
  890. See Also
  891. ========
  892. nyquist_plot, bode_plot
  893. """
  894. _check_system(system)
  895. magnitude_dB_expr, phase_deg_expr, w = nichols_plot_expr(system)
  896. w_values = [(w, initial_omega, final_omega)]
  897. p = plot_parametric(
  898. (phase_deg_expr, magnitude_dB_expr),
  899. *w_values,
  900. show=False,
  901. line_color=color,
  902. title=f'Nichols Plot of ${latex(system)}$',
  903. xlabel='Phase [deg]',
  904. ylabel='Magnitude [dB]',
  905. size=(6,5),
  906. kwargs=kwargs)
  907. if show:
  908. p.show()
  909. return
  910. return p