_lti_conversion.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. """
  2. ltisys -- a collection of functions to convert linear time invariant systems
  3. from one representation to another.
  4. """
  5. import numpy
  6. import numpy as np
  7. from numpy import (r_, eye, atleast_2d, poly, dot,
  8. asarray, prod, zeros, array, outer)
  9. from scipy import linalg
  10. from ._filter_design import tf2zpk, zpk2tf, normalize
  11. __all__ = ['tf2ss', 'abcd_normalize', 'ss2tf', 'zpk2ss', 'ss2zpk',
  12. 'cont2discrete']
  13. def tf2ss(num, den):
  14. r"""Transfer function to state-space representation.
  15. Parameters
  16. ----------
  17. num, den : array_like
  18. Sequences representing the coefficients of the numerator and
  19. denominator polynomials, in order of descending degree. The
  20. denominator needs to be at least as long as the numerator.
  21. Returns
  22. -------
  23. A, B, C, D : ndarray
  24. State space representation of the system, in controller canonical
  25. form.
  26. Examples
  27. --------
  28. Convert the transfer function:
  29. .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
  30. >>> num = [1, 3, 3]
  31. >>> den = [1, 2, 1]
  32. to the state-space representation:
  33. .. math::
  34. \dot{\textbf{x}}(t) =
  35. \begin{bmatrix} -2 & -1 \\ 1 & 0 \end{bmatrix} \textbf{x}(t) +
  36. \begin{bmatrix} 1 \\ 0 \end{bmatrix} \textbf{u}(t) \\
  37. \textbf{y}(t) = \begin{bmatrix} 1 & 2 \end{bmatrix} \textbf{x}(t) +
  38. \begin{bmatrix} 1 \end{bmatrix} \textbf{u}(t)
  39. >>> from scipy.signal import tf2ss
  40. >>> A, B, C, D = tf2ss(num, den)
  41. >>> A
  42. array([[-2., -1.],
  43. [ 1., 0.]])
  44. >>> B
  45. array([[ 1.],
  46. [ 0.]])
  47. >>> C
  48. array([[ 1., 2.]])
  49. >>> D
  50. array([[ 1.]])
  51. """
  52. # Controller canonical state-space representation.
  53. # if M+1 = len(num) and K+1 = len(den) then we must have M <= K
  54. # states are found by asserting that X(s) = U(s) / D(s)
  55. # then Y(s) = N(s) * X(s)
  56. #
  57. # A, B, C, and D follow quite naturally.
  58. #
  59. num, den = normalize(num, den) # Strips zeros, checks arrays
  60. nn = len(num.shape)
  61. if nn == 1:
  62. num = asarray([num], num.dtype)
  63. M = num.shape[1]
  64. K = len(den)
  65. if M > K:
  66. msg = "Improper transfer function. `num` is longer than `den`."
  67. raise ValueError(msg)
  68. if M == 0 or K == 0: # Null system
  69. return (array([], float), array([], float), array([], float),
  70. array([], float))
  71. # pad numerator to have same number of columns has denominator
  72. num = r_['-1', zeros((num.shape[0], K - M), num.dtype), num]
  73. if num.shape[-1] > 0:
  74. D = atleast_2d(num[:, 0])
  75. else:
  76. # We don't assign it an empty array because this system
  77. # is not 'null'. It just doesn't have a non-zero D
  78. # matrix. Thus, it should have a non-zero shape so that
  79. # it can be operated on by functions like 'ss2tf'
  80. D = array([[0]], float)
  81. if K == 1:
  82. D = D.reshape(num.shape)
  83. return (zeros((1, 1)), zeros((1, D.shape[1])),
  84. zeros((D.shape[0], 1)), D)
  85. frow = -array([den[1:]])
  86. A = r_[frow, eye(K - 2, K - 1)]
  87. B = eye(K - 1, 1)
  88. C = num[:, 1:] - outer(num[:, 0], den[1:])
  89. D = D.reshape((C.shape[0], B.shape[1]))
  90. return A, B, C, D
  91. def _none_to_empty_2d(arg):
  92. if arg is None:
  93. return zeros((0, 0))
  94. else:
  95. return arg
  96. def _atleast_2d_or_none(arg):
  97. if arg is not None:
  98. return atleast_2d(arg)
  99. def _shape_or_none(M):
  100. if M is not None:
  101. return M.shape
  102. else:
  103. return (None,) * 2
  104. def _choice_not_none(*args):
  105. for arg in args:
  106. if arg is not None:
  107. return arg
  108. def _restore(M, shape):
  109. if M.shape == (0, 0):
  110. return zeros(shape)
  111. else:
  112. if M.shape != shape:
  113. raise ValueError("The input arrays have incompatible shapes.")
  114. return M
  115. def abcd_normalize(A=None, B=None, C=None, D=None):
  116. """Check state-space matrices and ensure they are 2-D.
  117. If enough information on the system is provided, that is, enough
  118. properly-shaped arrays are passed to the function, the missing ones
  119. are built from this information, ensuring the correct number of
  120. rows and columns. Otherwise a ValueError is raised.
  121. Parameters
  122. ----------
  123. A, B, C, D : array_like, optional
  124. State-space matrices. All of them are None (missing) by default.
  125. See `ss2tf` for format.
  126. Returns
  127. -------
  128. A, B, C, D : array
  129. Properly shaped state-space matrices.
  130. Raises
  131. ------
  132. ValueError
  133. If not enough information on the system was provided.
  134. """
  135. A, B, C, D = map(_atleast_2d_or_none, (A, B, C, D))
  136. MA, NA = _shape_or_none(A)
  137. MB, NB = _shape_or_none(B)
  138. MC, NC = _shape_or_none(C)
  139. MD, ND = _shape_or_none(D)
  140. p = _choice_not_none(MA, MB, NC)
  141. q = _choice_not_none(NB, ND)
  142. r = _choice_not_none(MC, MD)
  143. if p is None or q is None or r is None:
  144. raise ValueError("Not enough information on the system.")
  145. A, B, C, D = map(_none_to_empty_2d, (A, B, C, D))
  146. A = _restore(A, (p, p))
  147. B = _restore(B, (p, q))
  148. C = _restore(C, (r, p))
  149. D = _restore(D, (r, q))
  150. return A, B, C, D
  151. def ss2tf(A, B, C, D, input=0):
  152. r"""State-space to transfer function.
  153. A, B, C, D defines a linear state-space system with `p` inputs,
  154. `q` outputs, and `n` state variables.
  155. Parameters
  156. ----------
  157. A : array_like
  158. State (or system) matrix of shape ``(n, n)``
  159. B : array_like
  160. Input matrix of shape ``(n, p)``
  161. C : array_like
  162. Output matrix of shape ``(q, n)``
  163. D : array_like
  164. Feedthrough (or feedforward) matrix of shape ``(q, p)``
  165. input : int, optional
  166. For multiple-input systems, the index of the input to use.
  167. Returns
  168. -------
  169. num : 2-D ndarray
  170. Numerator(s) of the resulting transfer function(s). `num` has one row
  171. for each of the system's outputs. Each row is a sequence representation
  172. of the numerator polynomial.
  173. den : 1-D ndarray
  174. Denominator of the resulting transfer function(s). `den` is a sequence
  175. representation of the denominator polynomial.
  176. Examples
  177. --------
  178. Convert the state-space representation:
  179. .. math::
  180. \dot{\textbf{x}}(t) =
  181. \begin{bmatrix} -2 & -1 \\ 1 & 0 \end{bmatrix} \textbf{x}(t) +
  182. \begin{bmatrix} 1 \\ 0 \end{bmatrix} \textbf{u}(t) \\
  183. \textbf{y}(t) = \begin{bmatrix} 1 & 2 \end{bmatrix} \textbf{x}(t) +
  184. \begin{bmatrix} 1 \end{bmatrix} \textbf{u}(t)
  185. >>> A = [[-2, -1], [1, 0]]
  186. >>> B = [[1], [0]] # 2-D column vector
  187. >>> C = [[1, 2]] # 2-D row vector
  188. >>> D = 1
  189. to the transfer function:
  190. .. math:: H(s) = \frac{s^2 + 3s + 3}{s^2 + 2s + 1}
  191. >>> from scipy.signal import ss2tf
  192. >>> ss2tf(A, B, C, D)
  193. (array([[1., 3., 3.]]), array([ 1., 2., 1.]))
  194. """
  195. # transfer function is C (sI - A)**(-1) B + D
  196. # Check consistency and make them all rank-2 arrays
  197. A, B, C, D = abcd_normalize(A, B, C, D)
  198. nout, nin = D.shape
  199. if input >= nin:
  200. raise ValueError("System does not have the input specified.")
  201. # make SIMO from possibly MIMO system.
  202. B = B[:, input:input + 1]
  203. D = D[:, input:input + 1]
  204. try:
  205. den = poly(A)
  206. except ValueError:
  207. den = 1
  208. if (prod(B.shape, axis=0) == 0) and (prod(C.shape, axis=0) == 0):
  209. num = numpy.ravel(D)
  210. if (prod(D.shape, axis=0) == 0) and (prod(A.shape, axis=0) == 0):
  211. den = []
  212. return num, den
  213. num_states = A.shape[0]
  214. type_test = A[:, 0] + B[:, 0] + C[0, :] + D + 0.0
  215. num = numpy.empty((nout, num_states + 1), type_test.dtype)
  216. for k in range(nout):
  217. Ck = atleast_2d(C[k, :])
  218. num[k] = poly(A - dot(B, Ck)) + (D[k] - 1) * den
  219. return num, den
  220. def zpk2ss(z, p, k):
  221. """Zero-pole-gain representation to state-space representation
  222. Parameters
  223. ----------
  224. z, p : sequence
  225. Zeros and poles.
  226. k : float
  227. System gain.
  228. Returns
  229. -------
  230. A, B, C, D : ndarray
  231. State space representation of the system, in controller canonical
  232. form.
  233. """
  234. return tf2ss(*zpk2tf(z, p, k))
  235. def ss2zpk(A, B, C, D, input=0):
  236. """State-space representation to zero-pole-gain representation.
  237. A, B, C, D defines a linear state-space system with `p` inputs,
  238. `q` outputs, and `n` state variables.
  239. Parameters
  240. ----------
  241. A : array_like
  242. State (or system) matrix of shape ``(n, n)``
  243. B : array_like
  244. Input matrix of shape ``(n, p)``
  245. C : array_like
  246. Output matrix of shape ``(q, n)``
  247. D : array_like
  248. Feedthrough (or feedforward) matrix of shape ``(q, p)``
  249. input : int, optional
  250. For multiple-input systems, the index of the input to use.
  251. Returns
  252. -------
  253. z, p : sequence
  254. Zeros and poles.
  255. k : float
  256. System gain.
  257. """
  258. return tf2zpk(*ss2tf(A, B, C, D, input=input))
  259. def cont2discrete(system, dt, method="zoh", alpha=None):
  260. """
  261. Transform a continuous to a discrete state-space system.
  262. Parameters
  263. ----------
  264. system : a tuple describing the system or an instance of `lti`
  265. The following gives the number of elements in the tuple and
  266. the interpretation:
  267. * 1: (instance of `lti`)
  268. * 2: (num, den)
  269. * 3: (zeros, poles, gain)
  270. * 4: (A, B, C, D)
  271. dt : float
  272. The discretization time step.
  273. method : str, optional
  274. Which method to use:
  275. * gbt: generalized bilinear transformation
  276. * bilinear: Tustin's approximation ("gbt" with alpha=0.5)
  277. * euler: Euler (or forward differencing) method ("gbt" with alpha=0)
  278. * backward_diff: Backwards differencing ("gbt" with alpha=1.0)
  279. * zoh: zero-order hold (default)
  280. * foh: first-order hold (*versionadded: 1.3.0*)
  281. * impulse: equivalent impulse response (*versionadded: 1.3.0*)
  282. alpha : float within [0, 1], optional
  283. The generalized bilinear transformation weighting parameter, which
  284. should only be specified with method="gbt", and is ignored otherwise
  285. Returns
  286. -------
  287. sysd : tuple containing the discrete system
  288. Based on the input type, the output will be of the form
  289. * (num, den, dt) for transfer function input
  290. * (zeros, poles, gain, dt) for zeros-poles-gain input
  291. * (A, B, C, D, dt) for state-space system input
  292. Notes
  293. -----
  294. By default, the routine uses a Zero-Order Hold (zoh) method to perform
  295. the transformation. Alternatively, a generalized bilinear transformation
  296. may be used, which includes the common Tustin's bilinear approximation,
  297. an Euler's method technique, or a backwards differencing technique.
  298. The Zero-Order Hold (zoh) method is based on [1]_, the generalized bilinear
  299. approximation is based on [2]_ and [3]_, the First-Order Hold (foh) method
  300. is based on [4]_.
  301. References
  302. ----------
  303. .. [1] https://en.wikipedia.org/wiki/Discretization#Discretization_of_linear_state_space_models
  304. .. [2] http://techteach.no/publications/discretetime_signals_systems/discrete.pdf
  305. .. [3] G. Zhang, X. Chen, and T. Chen, Digital redesign via the generalized
  306. bilinear transformation, Int. J. Control, vol. 82, no. 4, pp. 741-754,
  307. 2009.
  308. (https://www.mypolyuweb.hk/~magzhang/Research/ZCC09_IJC.pdf)
  309. .. [4] G. F. Franklin, J. D. Powell, and M. L. Workman, Digital control
  310. of dynamic systems, 3rd ed. Menlo Park, Calif: Addison-Wesley,
  311. pp. 204-206, 1998.
  312. Examples
  313. --------
  314. We can transform a continuous state-space system to a discrete one:
  315. >>> import numpy as np
  316. >>> import matplotlib.pyplot as plt
  317. >>> from scipy.signal import cont2discrete, lti, dlti, dstep
  318. Define a continuous state-space system.
  319. >>> A = np.array([[0, 1],[-10., -3]])
  320. >>> B = np.array([[0],[10.]])
  321. >>> C = np.array([[1., 0]])
  322. >>> D = np.array([[0.]])
  323. >>> l_system = lti(A, B, C, D)
  324. >>> t, x = l_system.step(T=np.linspace(0, 5, 100))
  325. >>> fig, ax = plt.subplots()
  326. >>> ax.plot(t, x, label='Continuous', linewidth=3)
  327. Transform it to a discrete state-space system using several methods.
  328. >>> dt = 0.1
  329. >>> for method in ['zoh', 'bilinear', 'euler', 'backward_diff', 'foh', 'impulse']:
  330. ... d_system = cont2discrete((A, B, C, D), dt, method=method)
  331. ... s, x_d = dstep(d_system)
  332. ... ax.step(s, np.squeeze(x_d), label=method, where='post')
  333. >>> ax.axis([t[0], t[-1], x[0], 1.4])
  334. >>> ax.legend(loc='best')
  335. >>> fig.tight_layout()
  336. >>> plt.show()
  337. """
  338. if len(system) == 1:
  339. return system.to_discrete()
  340. if len(system) == 2:
  341. sysd = cont2discrete(tf2ss(system[0], system[1]), dt, method=method,
  342. alpha=alpha)
  343. return ss2tf(sysd[0], sysd[1], sysd[2], sysd[3]) + (dt,)
  344. elif len(system) == 3:
  345. sysd = cont2discrete(zpk2ss(system[0], system[1], system[2]), dt,
  346. method=method, alpha=alpha)
  347. return ss2zpk(sysd[0], sysd[1], sysd[2], sysd[3]) + (dt,)
  348. elif len(system) == 4:
  349. a, b, c, d = system
  350. else:
  351. raise ValueError("First argument must either be a tuple of 2 (tf), "
  352. "3 (zpk), or 4 (ss) arrays.")
  353. if method == 'gbt':
  354. if alpha is None:
  355. raise ValueError("Alpha parameter must be specified for the "
  356. "generalized bilinear transform (gbt) method")
  357. elif alpha < 0 or alpha > 1:
  358. raise ValueError("Alpha parameter must be within the interval "
  359. "[0,1] for the gbt method")
  360. if method == 'gbt':
  361. # This parameter is used repeatedly - compute once here
  362. ima = np.eye(a.shape[0]) - alpha*dt*a
  363. ad = linalg.solve(ima, np.eye(a.shape[0]) + (1.0-alpha)*dt*a)
  364. bd = linalg.solve(ima, dt*b)
  365. # Similarly solve for the output equation matrices
  366. cd = linalg.solve(ima.transpose(), c.transpose())
  367. cd = cd.transpose()
  368. dd = d + alpha*np.dot(c, bd)
  369. elif method == 'bilinear' or method == 'tustin':
  370. return cont2discrete(system, dt, method="gbt", alpha=0.5)
  371. elif method == 'euler' or method == 'forward_diff':
  372. return cont2discrete(system, dt, method="gbt", alpha=0.0)
  373. elif method == 'backward_diff':
  374. return cont2discrete(system, dt, method="gbt", alpha=1.0)
  375. elif method == 'zoh':
  376. # Build an exponential matrix
  377. em_upper = np.hstack((a, b))
  378. # Need to stack zeros under the a and b matrices
  379. em_lower = np.hstack((np.zeros((b.shape[1], a.shape[0])),
  380. np.zeros((b.shape[1], b.shape[1]))))
  381. em = np.vstack((em_upper, em_lower))
  382. ms = linalg.expm(dt * em)
  383. # Dispose of the lower rows
  384. ms = ms[:a.shape[0], :]
  385. ad = ms[:, 0:a.shape[1]]
  386. bd = ms[:, a.shape[1]:]
  387. cd = c
  388. dd = d
  389. elif method == 'foh':
  390. # Size parameters for convenience
  391. n = a.shape[0]
  392. m = b.shape[1]
  393. # Build an exponential matrix similar to 'zoh' method
  394. em_upper = linalg.block_diag(np.block([a, b]) * dt, np.eye(m))
  395. em_lower = zeros((m, n + 2 * m))
  396. em = np.block([[em_upper], [em_lower]])
  397. ms = linalg.expm(em)
  398. # Get the three blocks from upper rows
  399. ms11 = ms[:n, 0:n]
  400. ms12 = ms[:n, n:n + m]
  401. ms13 = ms[:n, n + m:]
  402. ad = ms11
  403. bd = ms12 - ms13 + ms11 @ ms13
  404. cd = c
  405. dd = d + c @ ms13
  406. elif method == 'impulse':
  407. if not np.allclose(d, 0):
  408. raise ValueError("Impulse method is only applicable"
  409. "to strictly proper systems")
  410. ad = linalg.expm(a * dt)
  411. bd = ad @ b * dt
  412. cd = c
  413. dd = c @ b * dt
  414. else:
  415. raise ValueError("Unknown transformation method '%s'" % method)
  416. return ad, bd, cd, dd, dt