_expm_frechet.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. """Frechet derivative of the matrix exponential."""
  2. import numpy as np
  3. import scipy.linalg
  4. from scipy._lib._util import _apply_over_batch
  5. __all__ = ['expm_frechet', 'expm_cond']
  6. @_apply_over_batch(('A', 2), ('E', 2))
  7. def expm_frechet(A, E, method=None, compute_expm=True, check_finite=True):
  8. """
  9. Frechet derivative of the matrix exponential of A in the direction E.
  10. Parameters
  11. ----------
  12. A : (N, N) array_like
  13. Matrix of which to take the matrix exponential.
  14. E : (N, N) array_like
  15. Matrix direction in which to take the Frechet derivative.
  16. method : str, optional
  17. Choice of algorithm. Should be one of
  18. - `SPS` (default)
  19. - `blockEnlarge`
  20. compute_expm : bool, optional
  21. Whether to compute also `expm_A` in addition to `expm_frechet_AE`.
  22. Default is True.
  23. check_finite : bool, optional
  24. Whether to check that the input matrix contains only finite numbers.
  25. Disabling may give a performance gain, but may result in problems
  26. (crashes, non-termination) if the inputs do contain infinities or NaNs.
  27. Returns
  28. -------
  29. expm_A : ndarray
  30. Matrix exponential of A.
  31. expm_frechet_AE : ndarray
  32. Frechet derivative of the matrix exponential of A in the direction E.
  33. For ``compute_expm = False``, only `expm_frechet_AE` is returned.
  34. See Also
  35. --------
  36. expm : Compute the exponential of a matrix.
  37. Notes
  38. -----
  39. This section describes the available implementations that can be selected
  40. by the `method` parameter. The default method is *SPS*.
  41. Method *blockEnlarge* is a naive algorithm.
  42. Method *SPS* is Scaling-Pade-Squaring [1]_.
  43. It is a sophisticated implementation which should take
  44. only about 3/8 as much time as the naive implementation.
  45. The asymptotics are the same.
  46. .. versionadded:: 0.13.0
  47. References
  48. ----------
  49. .. [1] Awad H. Al-Mohy and Nicholas J. Higham (2009)
  50. Computing the Frechet Derivative of the Matrix Exponential,
  51. with an application to Condition Number Estimation.
  52. SIAM Journal On Matrix Analysis and Applications.,
  53. 30 (4). pp. 1639-1657. ISSN 1095-7162
  54. Examples
  55. --------
  56. >>> import numpy as np
  57. >>> from scipy import linalg
  58. >>> rng = np.random.default_rng()
  59. >>> A = rng.standard_normal((3, 3))
  60. >>> E = rng.standard_normal((3, 3))
  61. >>> expm_A, expm_frechet_AE = linalg.expm_frechet(A, E)
  62. >>> expm_A.shape, expm_frechet_AE.shape
  63. ((3, 3), (3, 3))
  64. Create a 6x6 matrix containing [[A, E], [0, A]]:
  65. >>> M = np.zeros((6, 6))
  66. >>> M[:3, :3] = A
  67. >>> M[:3, 3:] = E
  68. >>> M[3:, 3:] = A
  69. >>> expm_M = linalg.expm(M)
  70. >>> np.allclose(expm_A, expm_M[:3, :3])
  71. True
  72. >>> np.allclose(expm_frechet_AE, expm_M[:3, 3:])
  73. True
  74. """
  75. if check_finite:
  76. A = np.asarray_chkfinite(A)
  77. E = np.asarray_chkfinite(E)
  78. else:
  79. A = np.asarray(A)
  80. E = np.asarray(E)
  81. if A.ndim != 2 or A.shape[0] != A.shape[1]:
  82. raise ValueError('expected A to be a square matrix')
  83. if E.ndim != 2 or E.shape[0] != E.shape[1]:
  84. raise ValueError('expected E to be a square matrix')
  85. if A.shape != E.shape:
  86. raise ValueError('expected A and E to be the same shape')
  87. if method is None:
  88. method = 'SPS'
  89. if method == 'SPS':
  90. expm_A, expm_frechet_AE = expm_frechet_algo_64(A, E)
  91. elif method == 'blockEnlarge':
  92. expm_A, expm_frechet_AE = expm_frechet_block_enlarge(A, E)
  93. else:
  94. raise ValueError(f'Unknown implementation {method}')
  95. if compute_expm:
  96. return expm_A, expm_frechet_AE
  97. else:
  98. return expm_frechet_AE
  99. def expm_frechet_block_enlarge(A, E):
  100. """
  101. This is a helper function, mostly for testing and profiling.
  102. Return expm(A), frechet(A, E)
  103. """
  104. n = A.shape[0]
  105. M = np.vstack([
  106. np.hstack([A, E]),
  107. np.hstack([np.zeros_like(A), A])])
  108. expm_M = scipy.linalg.expm(M)
  109. return expm_M[:n, :n], expm_M[:n, n:]
  110. """
  111. Maximal values ell_m of ||2**-s A|| such that the backward error bound
  112. does not exceed 2**-53.
  113. """
  114. ell_table_61 = (
  115. None,
  116. # 1
  117. 2.11e-8,
  118. 3.56e-4,
  119. 1.08e-2,
  120. 6.49e-2,
  121. 2.00e-1,
  122. 4.37e-1,
  123. 7.83e-1,
  124. 1.23e0,
  125. 1.78e0,
  126. 2.42e0,
  127. # 11
  128. 3.13e0,
  129. 3.90e0,
  130. 4.74e0,
  131. 5.63e0,
  132. 6.56e0,
  133. 7.52e0,
  134. 8.53e0,
  135. 9.56e0,
  136. 1.06e1,
  137. 1.17e1,
  138. )
  139. # The b vectors and U and V are copypasted
  140. # from scipy.sparse.linalg.matfuncs.py.
  141. # M, Lu, Lv follow (6.11), (6.12), (6.13), (3.3)
  142. def _diff_pade3(A, E, ident):
  143. b = (120., 60., 12., 1.)
  144. A2 = A.dot(A)
  145. M2 = np.dot(A, E) + np.dot(E, A)
  146. U = A.dot(b[3]*A2 + b[1]*ident)
  147. V = b[2]*A2 + b[0]*ident
  148. Lu = A.dot(b[3]*M2) + E.dot(b[3]*A2 + b[1]*ident)
  149. Lv = b[2]*M2
  150. return U, V, Lu, Lv
  151. def _diff_pade5(A, E, ident):
  152. b = (30240., 15120., 3360., 420., 30., 1.)
  153. A2 = A.dot(A)
  154. M2 = np.dot(A, E) + np.dot(E, A)
  155. A4 = np.dot(A2, A2)
  156. M4 = np.dot(A2, M2) + np.dot(M2, A2)
  157. U = A.dot(b[5]*A4 + b[3]*A2 + b[1]*ident)
  158. V = b[4]*A4 + b[2]*A2 + b[0]*ident
  159. Lu = (A.dot(b[5]*M4 + b[3]*M2) +
  160. E.dot(b[5]*A4 + b[3]*A2 + b[1]*ident))
  161. Lv = b[4]*M4 + b[2]*M2
  162. return U, V, Lu, Lv
  163. def _diff_pade7(A, E, ident):
  164. b = (17297280., 8648640., 1995840., 277200., 25200., 1512., 56., 1.)
  165. A2 = A.dot(A)
  166. M2 = np.dot(A, E) + np.dot(E, A)
  167. A4 = np.dot(A2, A2)
  168. M4 = np.dot(A2, M2) + np.dot(M2, A2)
  169. A6 = np.dot(A2, A4)
  170. M6 = np.dot(A4, M2) + np.dot(M4, A2)
  171. U = A.dot(b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident)
  172. V = b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
  173. Lu = (A.dot(b[7]*M6 + b[5]*M4 + b[3]*M2) +
  174. E.dot(b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident))
  175. Lv = b[6]*M6 + b[4]*M4 + b[2]*M2
  176. return U, V, Lu, Lv
  177. def _diff_pade9(A, E, ident):
  178. b = (17643225600., 8821612800., 2075673600., 302702400., 30270240.,
  179. 2162160., 110880., 3960., 90., 1.)
  180. A2 = A.dot(A)
  181. M2 = np.dot(A, E) + np.dot(E, A)
  182. A4 = np.dot(A2, A2)
  183. M4 = np.dot(A2, M2) + np.dot(M2, A2)
  184. A6 = np.dot(A2, A4)
  185. M6 = np.dot(A4, M2) + np.dot(M4, A2)
  186. A8 = np.dot(A4, A4)
  187. M8 = np.dot(A4, M4) + np.dot(M4, A4)
  188. U = A.dot(b[9]*A8 + b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident)
  189. V = b[8]*A8 + b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
  190. Lu = (A.dot(b[9]*M8 + b[7]*M6 + b[5]*M4 + b[3]*M2) +
  191. E.dot(b[9]*A8 + b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident))
  192. Lv = b[8]*M8 + b[6]*M6 + b[4]*M4 + b[2]*M2
  193. return U, V, Lu, Lv
  194. def expm_frechet_algo_64(A, E):
  195. n = A.shape[0]
  196. s = None
  197. ident = np.identity(n)
  198. A_norm_1 = scipy.linalg.norm(A, 1)
  199. m_pade_pairs = (
  200. (3, _diff_pade3),
  201. (5, _diff_pade5),
  202. (7, _diff_pade7),
  203. (9, _diff_pade9))
  204. for m, pade in m_pade_pairs:
  205. if A_norm_1 <= ell_table_61[m]:
  206. U, V, Lu, Lv = pade(A, E, ident)
  207. s = 0
  208. break
  209. if s is None:
  210. # scaling
  211. s = max(0, int(np.ceil(np.log2(A_norm_1 / ell_table_61[13]))))
  212. A = A * 2.0**-s
  213. E = E * 2.0**-s
  214. # pade order 13
  215. A2 = np.dot(A, A)
  216. M2 = np.dot(A, E) + np.dot(E, A)
  217. A4 = np.dot(A2, A2)
  218. M4 = np.dot(A2, M2) + np.dot(M2, A2)
  219. A6 = np.dot(A2, A4)
  220. M6 = np.dot(A4, M2) + np.dot(M4, A2)
  221. b = (64764752532480000., 32382376266240000., 7771770303897600.,
  222. 1187353796428800., 129060195264000., 10559470521600.,
  223. 670442572800., 33522128640., 1323241920., 40840800., 960960.,
  224. 16380., 182., 1.)
  225. W1 = b[13]*A6 + b[11]*A4 + b[9]*A2
  226. W2 = b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident
  227. Z1 = b[12]*A6 + b[10]*A4 + b[8]*A2
  228. Z2 = b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
  229. W = np.dot(A6, W1) + W2
  230. U = np.dot(A, W)
  231. V = np.dot(A6, Z1) + Z2
  232. Lw1 = b[13]*M6 + b[11]*M4 + b[9]*M2
  233. Lw2 = b[7]*M6 + b[5]*M4 + b[3]*M2
  234. Lz1 = b[12]*M6 + b[10]*M4 + b[8]*M2
  235. Lz2 = b[6]*M6 + b[4]*M4 + b[2]*M2
  236. Lw = np.dot(A6, Lw1) + np.dot(M6, W1) + Lw2
  237. Lu = np.dot(A, Lw) + np.dot(E, W)
  238. Lv = np.dot(A6, Lz1) + np.dot(M6, Z1) + Lz2
  239. # factor once and solve twice
  240. lu_piv = scipy.linalg.lu_factor(-U + V)
  241. R = scipy.linalg.lu_solve(lu_piv, U + V)
  242. L = scipy.linalg.lu_solve(lu_piv, Lu + Lv + np.dot((Lu - Lv), R))
  243. # squaring
  244. for k in range(s):
  245. L = np.dot(R, L) + np.dot(L, R)
  246. R = np.dot(R, R)
  247. return R, L
  248. def vec(M):
  249. """
  250. Stack columns of M to construct a single vector.
  251. This is somewhat standard notation in linear algebra.
  252. Parameters
  253. ----------
  254. M : 2-D array_like
  255. Input matrix
  256. Returns
  257. -------
  258. v : 1-D ndarray
  259. Output vector
  260. """
  261. return M.T.ravel()
  262. def expm_frechet_kronform(A, method=None, check_finite=True):
  263. """
  264. Construct the Kronecker form of the Frechet derivative of expm.
  265. Parameters
  266. ----------
  267. A : array_like with shape (N, N)
  268. Matrix to be expm'd.
  269. method : str, optional
  270. Extra keyword to be passed to expm_frechet.
  271. check_finite : bool, optional
  272. Whether to check that the input matrix contains only finite numbers.
  273. Disabling may give a performance gain, but may result in problems
  274. (crashes, non-termination) if the inputs do contain infinities or NaNs.
  275. Returns
  276. -------
  277. K : 2-D ndarray with shape (N*N, N*N)
  278. Kronecker form of the Frechet derivative of the matrix exponential.
  279. Notes
  280. -----
  281. This function is used to help compute the condition number
  282. of the matrix exponential.
  283. See Also
  284. --------
  285. expm : Compute a matrix exponential.
  286. expm_frechet : Compute the Frechet derivative of the matrix exponential.
  287. expm_cond : Compute the relative condition number of the matrix exponential
  288. in the Frobenius norm.
  289. """
  290. if check_finite:
  291. A = np.asarray_chkfinite(A)
  292. else:
  293. A = np.asarray(A)
  294. if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
  295. raise ValueError('expected a square matrix')
  296. n = A.shape[0]
  297. ident = np.identity(n)
  298. cols = []
  299. for i in range(n):
  300. for j in range(n):
  301. E = np.outer(ident[i], ident[j])
  302. F = expm_frechet(A, E,
  303. method=method, compute_expm=False, check_finite=False)
  304. cols.append(vec(F))
  305. return np.vstack(cols).T
  306. @_apply_over_batch(('A', 2))
  307. def expm_cond(A, check_finite=True):
  308. """
  309. Relative condition number of the matrix exponential in the Frobenius norm.
  310. Parameters
  311. ----------
  312. A : 2-D array_like
  313. Square input matrix with shape (N, N).
  314. check_finite : bool, optional
  315. Whether to check that the input matrix contains only finite numbers.
  316. Disabling may give a performance gain, but may result in problems
  317. (crashes, non-termination) if the inputs do contain infinities or NaNs.
  318. Returns
  319. -------
  320. kappa : float
  321. The relative condition number of the matrix exponential
  322. in the Frobenius norm
  323. See Also
  324. --------
  325. expm : Compute the exponential of a matrix.
  326. expm_frechet : Compute the Frechet derivative of the matrix exponential.
  327. Notes
  328. -----
  329. A faster estimate for the condition number in the 1-norm
  330. has been published but is not yet implemented in SciPy.
  331. .. versionadded:: 0.14.0
  332. Examples
  333. --------
  334. >>> import numpy as np
  335. >>> from scipy.linalg import expm_cond
  336. >>> A = np.array([[-0.3, 0.2, 0.6], [0.6, 0.3, -0.1], [-0.7, 1.2, 0.9]])
  337. >>> k = expm_cond(A)
  338. >>> k
  339. 1.7787805864469866
  340. """
  341. if check_finite:
  342. A = np.asarray_chkfinite(A)
  343. else:
  344. A = np.asarray(A)
  345. if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
  346. raise ValueError('expected a square matrix')
  347. X = scipy.linalg.expm(A)
  348. K = expm_frechet_kronform(A, check_finite=False)
  349. # The following norm choices are deliberate.
  350. # The norms of A and X are Frobenius norms,
  351. # and the norm of K is the induced 2-norm.
  352. A_norm = scipy.linalg.norm(A, 'fro')
  353. X_norm = scipy.linalg.norm(X, 'fro')
  354. K_norm = scipy.linalg.norm(K, 2)
  355. kappa = (K_norm * A_norm) / X_norm
  356. return kappa