123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- """Frechet derivative of the matrix exponential."""
- import numpy as np
- import scipy.linalg
- __all__ = ['expm_frechet', 'expm_cond']
- def expm_frechet(A, E, method=None, compute_expm=True, check_finite=True):
- """
- Frechet derivative of the matrix exponential of A in the direction E.
- Parameters
- ----------
- A : (N, N) array_like
- Matrix of which to take the matrix exponential.
- E : (N, N) array_like
- Matrix direction in which to take the Frechet derivative.
- method : str, optional
- Choice of algorithm. Should be one of
- - `SPS` (default)
- - `blockEnlarge`
- compute_expm : bool, optional
- Whether to compute also `expm_A` in addition to `expm_frechet_AE`.
- Default is True.
- check_finite : bool, optional
- Whether to check that the input matrix contains only finite numbers.
- Disabling may give a performance gain, but may result in problems
- (crashes, non-termination) if the inputs do contain infinities or NaNs.
- Returns
- -------
- expm_A : ndarray
- Matrix exponential of A.
- expm_frechet_AE : ndarray
- Frechet derivative of the matrix exponential of A in the direction E.
- For ``compute_expm = False``, only `expm_frechet_AE` is returned.
- See Also
- --------
- expm : Compute the exponential of a matrix.
- Notes
- -----
- This section describes the available implementations that can be selected
- by the `method` parameter. The default method is *SPS*.
- Method *blockEnlarge* is a naive algorithm.
- Method *SPS* is Scaling-Pade-Squaring [1]_.
- It is a sophisticated implementation which should take
- only about 3/8 as much time as the naive implementation.
- The asymptotics are the same.
- .. versionadded:: 0.13.0
- References
- ----------
- .. [1] Awad H. Al-Mohy and Nicholas J. Higham (2009)
- Computing the Frechet Derivative of the Matrix Exponential,
- with an application to Condition Number Estimation.
- SIAM Journal On Matrix Analysis and Applications.,
- 30 (4). pp. 1639-1657. ISSN 1095-7162
- Examples
- --------
- >>> import numpy as np
- >>> from scipy import linalg
- >>> rng = np.random.default_rng()
- >>> A = rng.standard_normal((3, 3))
- >>> E = rng.standard_normal((3, 3))
- >>> expm_A, expm_frechet_AE = linalg.expm_frechet(A, E)
- >>> expm_A.shape, expm_frechet_AE.shape
- ((3, 3), (3, 3))
- Create a 6x6 matrix containg [[A, E], [0, A]]:
- >>> M = np.zeros((6, 6))
- >>> M[:3, :3] = A
- >>> M[:3, 3:] = E
- >>> M[3:, 3:] = A
- >>> expm_M = linalg.expm(M)
- >>> np.allclose(expm_A, expm_M[:3, :3])
- True
- >>> np.allclose(expm_frechet_AE, expm_M[:3, 3:])
- True
- """
- if check_finite:
- A = np.asarray_chkfinite(A)
- E = np.asarray_chkfinite(E)
- else:
- A = np.asarray(A)
- E = np.asarray(E)
- if A.ndim != 2 or A.shape[0] != A.shape[1]:
- raise ValueError('expected A to be a square matrix')
- if E.ndim != 2 or E.shape[0] != E.shape[1]:
- raise ValueError('expected E to be a square matrix')
- if A.shape != E.shape:
- raise ValueError('expected A and E to be the same shape')
- if method is None:
- method = 'SPS'
- if method == 'SPS':
- expm_A, expm_frechet_AE = expm_frechet_algo_64(A, E)
- elif method == 'blockEnlarge':
- expm_A, expm_frechet_AE = expm_frechet_block_enlarge(A, E)
- else:
- raise ValueError('Unknown implementation %s' % method)
- if compute_expm:
- return expm_A, expm_frechet_AE
- else:
- return expm_frechet_AE
- def expm_frechet_block_enlarge(A, E):
- """
- This is a helper function, mostly for testing and profiling.
- Return expm(A), frechet(A, E)
- """
- n = A.shape[0]
- M = np.vstack([
- np.hstack([A, E]),
- np.hstack([np.zeros_like(A), A])])
- expm_M = scipy.linalg.expm(M)
- return expm_M[:n, :n], expm_M[:n, n:]
- """
- Maximal values ell_m of ||2**-s A|| such that the backward error bound
- does not exceed 2**-53.
- """
- ell_table_61 = (
- None,
- # 1
- 2.11e-8,
- 3.56e-4,
- 1.08e-2,
- 6.49e-2,
- 2.00e-1,
- 4.37e-1,
- 7.83e-1,
- 1.23e0,
- 1.78e0,
- 2.42e0,
- # 11
- 3.13e0,
- 3.90e0,
- 4.74e0,
- 5.63e0,
- 6.56e0,
- 7.52e0,
- 8.53e0,
- 9.56e0,
- 1.06e1,
- 1.17e1,
- )
- # The b vectors and U and V are copypasted
- # from scipy.sparse.linalg.matfuncs.py.
- # M, Lu, Lv follow (6.11), (6.12), (6.13), (3.3)
- def _diff_pade3(A, E, ident):
- b = (120., 60., 12., 1.)
- A2 = A.dot(A)
- M2 = np.dot(A, E) + np.dot(E, A)
- U = A.dot(b[3]*A2 + b[1]*ident)
- V = b[2]*A2 + b[0]*ident
- Lu = A.dot(b[3]*M2) + E.dot(b[3]*A2 + b[1]*ident)
- Lv = b[2]*M2
- return U, V, Lu, Lv
- def _diff_pade5(A, E, ident):
- b = (30240., 15120., 3360., 420., 30., 1.)
- A2 = A.dot(A)
- M2 = np.dot(A, E) + np.dot(E, A)
- A4 = np.dot(A2, A2)
- M4 = np.dot(A2, M2) + np.dot(M2, A2)
- U = A.dot(b[5]*A4 + b[3]*A2 + b[1]*ident)
- V = b[4]*A4 + b[2]*A2 + b[0]*ident
- Lu = (A.dot(b[5]*M4 + b[3]*M2) +
- E.dot(b[5]*A4 + b[3]*A2 + b[1]*ident))
- Lv = b[4]*M4 + b[2]*M2
- return U, V, Lu, Lv
- def _diff_pade7(A, E, ident):
- b = (17297280., 8648640., 1995840., 277200., 25200., 1512., 56., 1.)
- A2 = A.dot(A)
- M2 = np.dot(A, E) + np.dot(E, A)
- A4 = np.dot(A2, A2)
- M4 = np.dot(A2, M2) + np.dot(M2, A2)
- A6 = np.dot(A2, A4)
- M6 = np.dot(A4, M2) + np.dot(M4, A2)
- U = A.dot(b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident)
- V = b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
- Lu = (A.dot(b[7]*M6 + b[5]*M4 + b[3]*M2) +
- E.dot(b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident))
- Lv = b[6]*M6 + b[4]*M4 + b[2]*M2
- return U, V, Lu, Lv
- def _diff_pade9(A, E, ident):
- b = (17643225600., 8821612800., 2075673600., 302702400., 30270240.,
- 2162160., 110880., 3960., 90., 1.)
- A2 = A.dot(A)
- M2 = np.dot(A, E) + np.dot(E, A)
- A4 = np.dot(A2, A2)
- M4 = np.dot(A2, M2) + np.dot(M2, A2)
- A6 = np.dot(A2, A4)
- M6 = np.dot(A4, M2) + np.dot(M4, A2)
- A8 = np.dot(A4, A4)
- M8 = np.dot(A4, M4) + np.dot(M4, A4)
- U = A.dot(b[9]*A8 + b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident)
- V = b[8]*A8 + b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
- Lu = (A.dot(b[9]*M8 + b[7]*M6 + b[5]*M4 + b[3]*M2) +
- E.dot(b[9]*A8 + b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident))
- Lv = b[8]*M8 + b[6]*M6 + b[4]*M4 + b[2]*M2
- return U, V, Lu, Lv
- def expm_frechet_algo_64(A, E):
- n = A.shape[0]
- s = None
- ident = np.identity(n)
- A_norm_1 = scipy.linalg.norm(A, 1)
- m_pade_pairs = (
- (3, _diff_pade3),
- (5, _diff_pade5),
- (7, _diff_pade7),
- (9, _diff_pade9))
- for m, pade in m_pade_pairs:
- if A_norm_1 <= ell_table_61[m]:
- U, V, Lu, Lv = pade(A, E, ident)
- s = 0
- break
- if s is None:
- # scaling
- s = max(0, int(np.ceil(np.log2(A_norm_1 / ell_table_61[13]))))
- A = A * 2.0**-s
- E = E * 2.0**-s
- # pade order 13
- A2 = np.dot(A, A)
- M2 = np.dot(A, E) + np.dot(E, A)
- A4 = np.dot(A2, A2)
- M4 = np.dot(A2, M2) + np.dot(M2, A2)
- A6 = np.dot(A2, A4)
- M6 = np.dot(A4, M2) + np.dot(M4, A2)
- b = (64764752532480000., 32382376266240000., 7771770303897600.,
- 1187353796428800., 129060195264000., 10559470521600.,
- 670442572800., 33522128640., 1323241920., 40840800., 960960.,
- 16380., 182., 1.)
- W1 = b[13]*A6 + b[11]*A4 + b[9]*A2
- W2 = b[7]*A6 + b[5]*A4 + b[3]*A2 + b[1]*ident
- Z1 = b[12]*A6 + b[10]*A4 + b[8]*A2
- Z2 = b[6]*A6 + b[4]*A4 + b[2]*A2 + b[0]*ident
- W = np.dot(A6, W1) + W2
- U = np.dot(A, W)
- V = np.dot(A6, Z1) + Z2
- Lw1 = b[13]*M6 + b[11]*M4 + b[9]*M2
- Lw2 = b[7]*M6 + b[5]*M4 + b[3]*M2
- Lz1 = b[12]*M6 + b[10]*M4 + b[8]*M2
- Lz2 = b[6]*M6 + b[4]*M4 + b[2]*M2
- Lw = np.dot(A6, Lw1) + np.dot(M6, W1) + Lw2
- Lu = np.dot(A, Lw) + np.dot(E, W)
- Lv = np.dot(A6, Lz1) + np.dot(M6, Z1) + Lz2
- # factor once and solve twice
- lu_piv = scipy.linalg.lu_factor(-U + V)
- R = scipy.linalg.lu_solve(lu_piv, U + V)
- L = scipy.linalg.lu_solve(lu_piv, Lu + Lv + np.dot((Lu - Lv), R))
- # squaring
- for k in range(s):
- L = np.dot(R, L) + np.dot(L, R)
- R = np.dot(R, R)
- return R, L
- def vec(M):
- """
- Stack columns of M to construct a single vector.
- This is somewhat standard notation in linear algebra.
- Parameters
- ----------
- M : 2-D array_like
- Input matrix
- Returns
- -------
- v : 1-D ndarray
- Output vector
- """
- return M.T.ravel()
- def expm_frechet_kronform(A, method=None, check_finite=True):
- """
- Construct the Kronecker form of the Frechet derivative of expm.
- Parameters
- ----------
- A : array_like with shape (N, N)
- Matrix to be expm'd.
- method : str, optional
- Extra keyword to be passed to expm_frechet.
- check_finite : bool, optional
- Whether to check that the input matrix contains only finite numbers.
- Disabling may give a performance gain, but may result in problems
- (crashes, non-termination) if the inputs do contain infinities or NaNs.
- Returns
- -------
- K : 2-D ndarray with shape (N*N, N*N)
- Kronecker form of the Frechet derivative of the matrix exponential.
- Notes
- -----
- This function is used to help compute the condition number
- of the matrix exponential.
- See Also
- --------
- expm : Compute a matrix exponential.
- expm_frechet : Compute the Frechet derivative of the matrix exponential.
- expm_cond : Compute the relative condition number of the matrix exponential
- in the Frobenius norm.
- """
- if check_finite:
- A = np.asarray_chkfinite(A)
- else:
- A = np.asarray(A)
- if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
- raise ValueError('expected a square matrix')
- n = A.shape[0]
- ident = np.identity(n)
- cols = []
- for i in range(n):
- for j in range(n):
- E = np.outer(ident[i], ident[j])
- F = expm_frechet(A, E,
- method=method, compute_expm=False, check_finite=False)
- cols.append(vec(F))
- return np.vstack(cols).T
- def expm_cond(A, check_finite=True):
- """
- Relative condition number of the matrix exponential in the Frobenius norm.
- Parameters
- ----------
- A : 2-D array_like
- Square input matrix with shape (N, N).
- check_finite : bool, optional
- Whether to check that the input matrix contains only finite numbers.
- Disabling may give a performance gain, but may result in problems
- (crashes, non-termination) if the inputs do contain infinities or NaNs.
- Returns
- -------
- kappa : float
- The relative condition number of the matrix exponential
- in the Frobenius norm
- See Also
- --------
- expm : Compute the exponential of a matrix.
- expm_frechet : Compute the Frechet derivative of the matrix exponential.
- Notes
- -----
- A faster estimate for the condition number in the 1-norm
- has been published but is not yet implemented in SciPy.
- .. versionadded:: 0.14.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.linalg import expm_cond
- >>> A = np.array([[-0.3, 0.2, 0.6], [0.6, 0.3, -0.1], [-0.7, 1.2, 0.9]])
- >>> k = expm_cond(A)
- >>> k
- 1.7787805864469866
- """
- if check_finite:
- A = np.asarray_chkfinite(A)
- else:
- A = np.asarray(A)
- if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
- raise ValueError('expected a square matrix')
- X = scipy.linalg.expm(A)
- K = expm_frechet_kronform(A, check_finite=False)
- # The following norm choices are deliberate.
- # The norms of A and X are Frobenius norms,
- # and the norm of K is the induced 2-norm.
- A_norm = scipy.linalg.norm(A, 'fro')
- X_norm = scipy.linalg.norm(X, 'fro')
- K_norm = scipy.linalg.norm(K, 2)
- kappa = (K_norm * A_norm) / X_norm
- return kappa
|