lti_conversion.py 14 KB

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