control_plots.py 31 KB

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