test_cont2discrete.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import numpy as np
  2. from numpy.testing import \
  3. assert_array_almost_equal, assert_almost_equal, \
  4. assert_allclose, assert_equal
  5. import pytest
  6. from scipy.signal import cont2discrete as c2d
  7. from scipy.signal import dlsim, ss2tf, ss2zpk, lsim2, lti
  8. from scipy.signal import tf2ss, impulse2, dimpulse, step2, dstep
  9. # Author: Jeffrey Armstrong <jeff@approximatrix.com>
  10. # March 29, 2011
  11. class TestC2D:
  12. def test_zoh(self):
  13. ac = np.eye(2)
  14. bc = np.full((2, 1), 0.5)
  15. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  16. dc = np.array([[0.0], [0.0], [-0.33]])
  17. ad_truth = 1.648721270700128 * np.eye(2)
  18. bd_truth = np.full((2, 1), 0.324360635350064)
  19. # c and d in discrete should be equal to their continuous counterparts
  20. dt_requested = 0.5
  21. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='zoh')
  22. assert_array_almost_equal(ad_truth, ad)
  23. assert_array_almost_equal(bd_truth, bd)
  24. assert_array_almost_equal(cc, cd)
  25. assert_array_almost_equal(dc, dd)
  26. assert_almost_equal(dt_requested, dt)
  27. def test_foh(self):
  28. ac = np.eye(2)
  29. bc = np.full((2, 1), 0.5)
  30. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  31. dc = np.array([[0.0], [0.0], [-0.33]])
  32. # True values are verified with Matlab
  33. ad_truth = 1.648721270700128 * np.eye(2)
  34. bd_truth = np.full((2, 1), 0.420839287058789)
  35. cd_truth = cc
  36. dd_truth = np.array([[0.260262223725224],
  37. [0.297442541400256],
  38. [-0.144098411624840]])
  39. dt_requested = 0.5
  40. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested, method='foh')
  41. assert_array_almost_equal(ad_truth, ad)
  42. assert_array_almost_equal(bd_truth, bd)
  43. assert_array_almost_equal(cd_truth, cd)
  44. assert_array_almost_equal(dd_truth, dd)
  45. assert_almost_equal(dt_requested, dt)
  46. def test_impulse(self):
  47. ac = np.eye(2)
  48. bc = np.full((2, 1), 0.5)
  49. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  50. dc = np.array([[0.0], [0.0], [0.0]])
  51. # True values are verified with Matlab
  52. ad_truth = 1.648721270700128 * np.eye(2)
  53. bd_truth = np.full((2, 1), 0.412180317675032)
  54. cd_truth = cc
  55. dd_truth = np.array([[0.4375], [0.5], [0.3125]])
  56. dt_requested = 0.5
  57. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  58. method='impulse')
  59. assert_array_almost_equal(ad_truth, ad)
  60. assert_array_almost_equal(bd_truth, bd)
  61. assert_array_almost_equal(cd_truth, cd)
  62. assert_array_almost_equal(dd_truth, dd)
  63. assert_almost_equal(dt_requested, dt)
  64. def test_gbt(self):
  65. ac = np.eye(2)
  66. bc = np.full((2, 1), 0.5)
  67. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  68. dc = np.array([[0.0], [0.0], [-0.33]])
  69. dt_requested = 0.5
  70. alpha = 1.0 / 3.0
  71. ad_truth = 1.6 * np.eye(2)
  72. bd_truth = np.full((2, 1), 0.3)
  73. cd_truth = np.array([[0.9, 1.2],
  74. [1.2, 1.2],
  75. [1.2, 0.3]])
  76. dd_truth = np.array([[0.175],
  77. [0.2],
  78. [-0.205]])
  79. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  80. method='gbt', alpha=alpha)
  81. assert_array_almost_equal(ad_truth, ad)
  82. assert_array_almost_equal(bd_truth, bd)
  83. assert_array_almost_equal(cd_truth, cd)
  84. assert_array_almost_equal(dd_truth, dd)
  85. def test_euler(self):
  86. ac = np.eye(2)
  87. bc = np.full((2, 1), 0.5)
  88. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  89. dc = np.array([[0.0], [0.0], [-0.33]])
  90. dt_requested = 0.5
  91. ad_truth = 1.5 * np.eye(2)
  92. bd_truth = np.full((2, 1), 0.25)
  93. cd_truth = np.array([[0.75, 1.0],
  94. [1.0, 1.0],
  95. [1.0, 0.25]])
  96. dd_truth = dc
  97. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  98. method='euler')
  99. assert_array_almost_equal(ad_truth, ad)
  100. assert_array_almost_equal(bd_truth, bd)
  101. assert_array_almost_equal(cd_truth, cd)
  102. assert_array_almost_equal(dd_truth, dd)
  103. assert_almost_equal(dt_requested, dt)
  104. def test_backward_diff(self):
  105. ac = np.eye(2)
  106. bc = np.full((2, 1), 0.5)
  107. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  108. dc = np.array([[0.0], [0.0], [-0.33]])
  109. dt_requested = 0.5
  110. ad_truth = 2.0 * np.eye(2)
  111. bd_truth = np.full((2, 1), 0.5)
  112. cd_truth = np.array([[1.5, 2.0],
  113. [2.0, 2.0],
  114. [2.0, 0.5]])
  115. dd_truth = np.array([[0.875],
  116. [1.0],
  117. [0.295]])
  118. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  119. method='backward_diff')
  120. assert_array_almost_equal(ad_truth, ad)
  121. assert_array_almost_equal(bd_truth, bd)
  122. assert_array_almost_equal(cd_truth, cd)
  123. assert_array_almost_equal(dd_truth, dd)
  124. def test_bilinear(self):
  125. ac = np.eye(2)
  126. bc = np.full((2, 1), 0.5)
  127. cc = np.array([[0.75, 1.0], [1.0, 1.0], [1.0, 0.25]])
  128. dc = np.array([[0.0], [0.0], [-0.33]])
  129. dt_requested = 0.5
  130. ad_truth = (5.0 / 3.0) * np.eye(2)
  131. bd_truth = np.full((2, 1), 1.0 / 3.0)
  132. cd_truth = np.array([[1.0, 4.0 / 3.0],
  133. [4.0 / 3.0, 4.0 / 3.0],
  134. [4.0 / 3.0, 1.0 / 3.0]])
  135. dd_truth = np.array([[0.291666666666667],
  136. [1.0 / 3.0],
  137. [-0.121666666666667]])
  138. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  139. method='bilinear')
  140. assert_array_almost_equal(ad_truth, ad)
  141. assert_array_almost_equal(bd_truth, bd)
  142. assert_array_almost_equal(cd_truth, cd)
  143. assert_array_almost_equal(dd_truth, dd)
  144. assert_almost_equal(dt_requested, dt)
  145. # Same continuous system again, but change sampling rate
  146. ad_truth = 1.4 * np.eye(2)
  147. bd_truth = np.full((2, 1), 0.2)
  148. cd_truth = np.array([[0.9, 1.2], [1.2, 1.2], [1.2, 0.3]])
  149. dd_truth = np.array([[0.175], [0.2], [-0.205]])
  150. dt_requested = 1.0 / 3.0
  151. ad, bd, cd, dd, dt = c2d((ac, bc, cc, dc), dt_requested,
  152. method='bilinear')
  153. assert_array_almost_equal(ad_truth, ad)
  154. assert_array_almost_equal(bd_truth, bd)
  155. assert_array_almost_equal(cd_truth, cd)
  156. assert_array_almost_equal(dd_truth, dd)
  157. assert_almost_equal(dt_requested, dt)
  158. def test_transferfunction(self):
  159. numc = np.array([0.25, 0.25, 0.5])
  160. denc = np.array([0.75, 0.75, 1.0])
  161. numd = np.array([[1.0 / 3.0, -0.427419169438754, 0.221654141101125]])
  162. dend = np.array([1.0, -1.351394049721225, 0.606530659712634])
  163. dt_requested = 0.5
  164. num, den, dt = c2d((numc, denc), dt_requested, method='zoh')
  165. assert_array_almost_equal(numd, num)
  166. assert_array_almost_equal(dend, den)
  167. assert_almost_equal(dt_requested, dt)
  168. def test_zerospolesgain(self):
  169. zeros_c = np.array([0.5, -0.5])
  170. poles_c = np.array([1.j / np.sqrt(2), -1.j / np.sqrt(2)])
  171. k_c = 1.0
  172. zeros_d = [1.23371727305860, 0.735356894461267]
  173. polls_d = [0.938148335039729 + 0.346233593780536j,
  174. 0.938148335039729 - 0.346233593780536j]
  175. k_d = 1.0
  176. dt_requested = 0.5
  177. zeros, poles, k, dt = c2d((zeros_c, poles_c, k_c), dt_requested,
  178. method='zoh')
  179. assert_array_almost_equal(zeros_d, zeros)
  180. assert_array_almost_equal(polls_d, poles)
  181. assert_almost_equal(k_d, k)
  182. assert_almost_equal(dt_requested, dt)
  183. def test_gbt_with_sio_tf_and_zpk(self):
  184. """Test method='gbt' with alpha=0.25 for tf and zpk cases."""
  185. # State space coefficients for the continuous SIO system.
  186. A = -1.0
  187. B = 1.0
  188. C = 1.0
  189. D = 0.5
  190. # The continuous transfer function coefficients.
  191. cnum, cden = ss2tf(A, B, C, D)
  192. # Continuous zpk representation
  193. cz, cp, ck = ss2zpk(A, B, C, D)
  194. h = 1.0
  195. alpha = 0.25
  196. # Explicit formulas, in the scalar case.
  197. Ad = (1 + (1 - alpha) * h * A) / (1 - alpha * h * A)
  198. Bd = h * B / (1 - alpha * h * A)
  199. Cd = C / (1 - alpha * h * A)
  200. Dd = D + alpha * C * Bd
  201. # Convert the explicit solution to tf
  202. dnum, dden = ss2tf(Ad, Bd, Cd, Dd)
  203. # Compute the discrete tf using cont2discrete.
  204. c2dnum, c2dden, dt = c2d((cnum, cden), h, method='gbt', alpha=alpha)
  205. assert_allclose(dnum, c2dnum)
  206. assert_allclose(dden, c2dden)
  207. # Convert explicit solution to zpk.
  208. dz, dp, dk = ss2zpk(Ad, Bd, Cd, Dd)
  209. # Compute the discrete zpk using cont2discrete.
  210. c2dz, c2dp, c2dk, dt = c2d((cz, cp, ck), h, method='gbt', alpha=alpha)
  211. assert_allclose(dz, c2dz)
  212. assert_allclose(dp, c2dp)
  213. assert_allclose(dk, c2dk)
  214. def test_discrete_approx(self):
  215. """
  216. Test that the solution to the discrete approximation of a continuous
  217. system actually approximates the solution to the continuous system.
  218. This is an indirect test of the correctness of the implementation
  219. of cont2discrete.
  220. """
  221. def u(t):
  222. return np.sin(2.5 * t)
  223. a = np.array([[-0.01]])
  224. b = np.array([[1.0]])
  225. c = np.array([[1.0]])
  226. d = np.array([[0.2]])
  227. x0 = 1.0
  228. t = np.linspace(0, 10.0, 101)
  229. dt = t[1] - t[0]
  230. u1 = u(t)
  231. # Use lsim2 to compute the solution to the continuous system.
  232. t, yout, xout = lsim2((a, b, c, d), T=t, U=u1, X0=x0,
  233. rtol=1e-9, atol=1e-11)
  234. # Convert the continuous system to a discrete approximation.
  235. dsys = c2d((a, b, c, d), dt, method='bilinear')
  236. # Use dlsim with the pairwise averaged input to compute the output
  237. # of the discrete system.
  238. u2 = 0.5 * (u1[:-1] + u1[1:])
  239. t2 = t[:-1]
  240. td2, yd2, xd2 = dlsim(dsys, u=u2.reshape(-1, 1), t=t2, x0=x0)
  241. # ymid is the average of consecutive terms of the "exact" output
  242. # computed by lsim2. This is what the discrete approximation
  243. # actually approximates.
  244. ymid = 0.5 * (yout[:-1] + yout[1:])
  245. assert_allclose(yd2.ravel(), ymid, rtol=1e-4)
  246. def test_simo_tf(self):
  247. # See gh-5753
  248. tf = ([[1, 0], [1, 1]], [1, 1])
  249. num, den, dt = c2d(tf, 0.01)
  250. assert_equal(dt, 0.01) # sanity check
  251. assert_allclose(den, [1, -0.990404983], rtol=1e-3)
  252. assert_allclose(num, [[1, -1], [1, -0.99004983]], rtol=1e-3)
  253. def test_multioutput(self):
  254. ts = 0.01 # time step
  255. tf = ([[1, -3], [1, 5]], [1, 1])
  256. num, den, dt = c2d(tf, ts)
  257. tf1 = (tf[0][0], tf[1])
  258. num1, den1, dt1 = c2d(tf1, ts)
  259. tf2 = (tf[0][1], tf[1])
  260. num2, den2, dt2 = c2d(tf2, ts)
  261. # Sanity checks
  262. assert_equal(dt, dt1)
  263. assert_equal(dt, dt2)
  264. # Check that we get the same results
  265. assert_allclose(num, np.vstack((num1, num2)), rtol=1e-13)
  266. # Single input, so the denominator should
  267. # not be multidimensional like the numerator
  268. assert_allclose(den, den1, rtol=1e-13)
  269. assert_allclose(den, den2, rtol=1e-13)
  270. class TestC2dLti:
  271. def test_c2d_ss(self):
  272. # StateSpace
  273. A = np.array([[-0.3, 0.1], [0.2, -0.7]])
  274. B = np.array([[0], [1]])
  275. C = np.array([[1, 0]])
  276. D = 0
  277. A_res = np.array([[0.985136404135682, 0.004876671474795],
  278. [0.009753342949590, 0.965629718236502]])
  279. B_res = np.array([[0.000122937599964], [0.049135527547844]])
  280. sys_ssc = lti(A, B, C, D)
  281. sys_ssd = sys_ssc.to_discrete(0.05)
  282. assert_allclose(sys_ssd.A, A_res)
  283. assert_allclose(sys_ssd.B, B_res)
  284. assert_allclose(sys_ssd.C, C)
  285. assert_allclose(sys_ssd.D, D)
  286. def test_c2d_tf(self):
  287. sys = lti([0.5, 0.3], [1.0, 0.4])
  288. sys = sys.to_discrete(0.005)
  289. # Matlab results
  290. num_res = np.array([0.5, -0.485149004980066])
  291. den_res = np.array([1.0, -0.980198673306755])
  292. # Somehow a lot of numerical errors
  293. assert_allclose(sys.den, den_res, atol=0.02)
  294. assert_allclose(sys.num, num_res, atol=0.02)
  295. class TestC2dInvariants:
  296. # Some test cases for checking the invariances.
  297. # Array of triplets: (system, sample time, number of samples)
  298. cases = [
  299. (tf2ss([1, 1], [1, 1.5, 1]), 0.25, 10),
  300. (tf2ss([1, 2], [1, 1.5, 3, 1]), 0.5, 10),
  301. (tf2ss(0.1, [1, 1, 2, 1]), 0.5, 10),
  302. ]
  303. # Some options for lsim2 and derived routines
  304. tolerances = {'rtol': 1e-9, 'atol': 1e-11}
  305. # Check that systems discretized with the impulse-invariant
  306. # method really hold the invariant
  307. @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
  308. def test_impulse_invariant(self, sys, sample_time, samples_number):
  309. time = np.arange(samples_number) * sample_time
  310. _, yout_cont = impulse2(sys, T=time, **self.tolerances)
  311. _, yout_disc = dimpulse(c2d(sys, sample_time, method='impulse'),
  312. n=len(time))
  313. assert_allclose(sample_time * yout_cont.ravel(), yout_disc[0].ravel())
  314. # Step invariant should hold for ZOH discretized systems
  315. @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
  316. def test_step_invariant(self, sys, sample_time, samples_number):
  317. time = np.arange(samples_number) * sample_time
  318. _, yout_cont = step2(sys, T=time, **self.tolerances)
  319. _, yout_disc = dstep(c2d(sys, sample_time, method='zoh'), n=len(time))
  320. assert_allclose(yout_cont.ravel(), yout_disc[0].ravel())
  321. # Linear invariant should hold for FOH discretized systems
  322. @pytest.mark.parametrize("sys,sample_time,samples_number", cases)
  323. def test_linear_invariant(self, sys, sample_time, samples_number):
  324. time = np.arange(samples_number) * sample_time
  325. _, yout_cont, _ = lsim2(sys, T=time, U=time, **self.tolerances)
  326. _, yout_disc, _ = dlsim(c2d(sys, sample_time, method='foh'), u=time)
  327. assert_allclose(yout_cont.ravel(), yout_disc.ravel())