test_cont2discrete.py 15 KB

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