def implicit_euler(A, F, M, U0, t0, t1, nt, mu=None, num_values=None, solver_options='operator'): assert isinstance(A, OperatorInterface) assert isinstance(F, (type(None), OperatorInterface, VectorArrayInterface)) assert isinstance(M, (type(None), OperatorInterface)) assert A.source == A.range num_values = num_values or nt + 1 dt = (t1 - t0) / nt DT = (t1 - t0) / (num_values - 1) if F is None: F_time_dep = False elif isinstance(F, OperatorInterface): assert F.range.dim == 1 assert F.source == A.range F_time_dep = F.parametric and '_t' in F.parameter_type if not F_time_dep: dt_F = F.as_vector(mu, space=A.source) * dt else: assert len(F) == 1 assert F in A.range F_time_dep = False dt_F = F * dt if M is None: from pymor.operators.constructions import IdentityOperator M = IdentityOperator(A.source) assert A.source == M.source == M.range assert not M.parametric assert U0 in A.source assert len(U0) == 1 A_time_dep = A.parametric and '_t' in A.parameter_type R = A.source.empty(reserve=nt+1) R.append(U0) options = A.solver_options if solver_options == 'operator' else \ M.solver_options if solver_options == 'mass' else \ solver_options M_dt_A = (M + A * dt).with_(solver_options=options) if not A_time_dep: M_dt_A = M_dt_A.assemble(mu) t = t0 U = U0.copy() for n in range(nt): t += dt mu['_t'] = t rhs = M.apply(U) if F_time_dep: dt_F = F.as_vector(mu, space=A.source) * dt if F: rhs += dt_F U = M_dt_A.apply_inverse(rhs, mu=mu) while t - t0 + (min(dt, DT) * 0.5) >= len(R) * DT: R.append(U) return R
def lradi(A, E, B, trans=False, options=None): """Find a factor of the solution of a Lyapunov equation using the low-rank ADI iteration as described in Algorithm 4.3 in [PK16]_. Parameters ---------- A The |Operator| A. E The |Operator| E or `None`. B The |Operator| B. trans If the dual equation needs to be solved. options The |solver_options| to use (see :func:`lyap_solver_options`). Returns ------- Z Low-rank factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ logger = getLogger('pymor.algorithms.lyapunov.lradi') shift_options = options['shift_options'][options['shifts']] if shift_options['type'] == 'projection_shifts': init_shifts = projection_shifts_init iteration_shifts = projection_shifts else: raise ValueError('Unknown lradi shift strategy') if E is None: E = IdentityOperator(A.source) if not trans: Z = A.source.empty(reserve=B.source.dim * options['maxiter']) W = B.as_range_array() else: Z = A.range.empty(reserve=B.range.dim * options['maxiter']) W = B.as_source_array() j = 0 shifts = init_shifts(A, E, W, shift_options) size_shift = shifts.size res = np.linalg.norm(W.gramian(), ord=2) init_res = res Btol = res * options['tol'] while res > Btol and j < options['maxiter']: if shifts[j].imag == 0: AaE = A + shifts[j].real * E if not trans: V = AaE.apply_inverse(W) W -= E.apply(V) * (2 * shifts[j].real) else: V = AaE.apply_inverse_adjoint(W) W -= E.apply_adjoint(V) * (2 * shifts[j].real) Z.append(V * np.sqrt(-2 * shifts[j].real)) j += 1 else: AaE = A + shifts[j] * E g = 2 * np.sqrt(-shifts[j].real) d = shifts[j].real / shifts[j].imag if not trans: V = AaE.apply_inverse(W) W += E.apply(V.real + V.imag * d) * g**2 else: V = AaE.apply_inverse_adjoint(W).conj() W += E.apply_adjoint(V.real + V.imag * d) * g**2 Z.append((V.real + V.imag * d) * g) Z.append(V.imag * (g * np.sqrt(d**2 + 1))) j += 2 if j >= size_shift: shifts = iteration_shifts(A, E, Z, W, shifts, shift_options) size_shift = shifts.size res = np.linalg.norm(W.gramian(), ord=2) logger.info("Relative residual at step {}: {:.5e}".format(j, res / init_res)) if res > Btol: logger.warning('Prescribed relative residual tolerance was not achieved ({:e} > {:e}) after ' '{} ADI steps.'.format(res / init_res, options['tol'], options['maxiter'])) return Z
def solve_lyap_lrcf(A, E, B, trans=False, options=None): """Compute an approximate low-rank solution of a Lyapunov equation. See :func:`pymor.algorithms.lyapunov.solve_lyap_lrcf` for a general description. This function uses the low-rank ADI iteration as described in Algorithm 4.3 in [PK16]_. Parameters ---------- A The |Operator| A. E The |Operator| E or `None`. B The operator B as a |VectorArray| from `A.source`. trans Whether the first |Operator| in the Lyapunov equation is transposed. options The solver options to use (see :func:`lyap_lrcf_solver_options`). Returns ------- Z Low-rank Cholesky factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ _solve_lyap_lrcf_check_args(A, E, B, trans) options = _parse_options(options, lyap_lrcf_solver_options(), 'lradi', None, False) logger = getLogger('pymor.algorithms.lradi.solve_lyap_lrcf') shift_options = options['shift_options'][options['shifts']] if shift_options['type'] == 'projection_shifts': init_shifts = projection_shifts_init iteration_shifts = projection_shifts else: raise ValueError('Unknown lradi shift strategy.') if E is None: E = IdentityOperator(A.source) Z = A.source.empty(reserve=len(B) * options['maxiter']) W = B.copy() j = 0 j_shift = 0 shifts = init_shifts(A, E, W, shift_options) res = np.linalg.norm(W.gramian(), ord=2) init_res = res Btol = res * options['tol'] while res > Btol and j < options['maxiter']: if shifts[j_shift].imag == 0: AaE = A + shifts[j_shift].real * E if not trans: V = AaE.apply_inverse(W) W -= E.apply(V) * (2 * shifts[j_shift].real) else: V = AaE.apply_inverse_adjoint(W) W -= E.apply_adjoint(V) * (2 * shifts[j_shift].real) Z.append(V * np.sqrt(-2 * shifts[j_shift].real)) j += 1 else: AaE = A + shifts[j_shift] * E gs = -4 * shifts[j_shift].real d = shifts[j_shift].real / shifts[j_shift].imag if not trans: V = AaE.apply_inverse(W) W += E.apply(V.real + V.imag * d) * gs else: V = AaE.apply_inverse_adjoint(W).conj() W += E.apply_adjoint(V.real + V.imag * d) * gs g = np.sqrt(gs) Z.append((V.real + V.imag * d) * g) Z.append(V.imag * (g * np.sqrt(d**2 + 1))) j += 2 j_shift += 1 res = np.linalg.norm(W.gramian(), ord=2) logger.info(f'Relative residual at step {j}: {res/init_res:.5e}') if j_shift >= shifts.size: shifts = iteration_shifts(A, E, V, shifts) j_shift = 0 if res > Btol: logger.warning( f'Prescribed relative residual tolerance was not achieved ' f'({res/init_res:e} > {options["tol"]:e}) after ' f'{options["maxiter"]} ADI steps.') return Z
def solve_ricc_lrcf(A, E, B, C, R=None, S=None, trans=False, options=None): """Compute an approximate low-rank solution of a Riccati equation. See :func:`pymor.algorithms.riccati.solve_ricc_lrcf` for a general description. This function is an implementation of Algorithm 2 in [BBKS18]_. Parameters ---------- A The |Operator| A. E The |Operator| E or `None`. B The operator B as a |VectorArray| from `A.source`. C The operator C as a |VectorArray| from `A.source`. R The operator R as a 2D |NumPy array| or `None`. S The operator S as a |VectorArray| from `A.source` or `None`. trans Whether the first |Operator| in the Riccati equation is transposed. options The solver options to use. (see :func:`ricc_lrcf_solver_options`) Returns ------- Z Low-rank Cholesky factor of the Riccati equation solution, |VectorArray| from `A.source`. """ _solve_ricc_check_args(A, E, B, C, None, None, trans) options = _parse_options(options, ricc_lrcf_solver_options(), 'lrradi', None, False) logger = getLogger('pymor.algorithms.lrradi.solve_ricc_lrcf') shift_options = options['shift_options'][options['shifts']] if shift_options['type'] == 'hamiltonian_shifts': init_shifts = hamiltonian_shifts_init iteration_shifts = hamiltonian_shifts else: raise ValueError('Unknown lrradi shift strategy.') if E is None: E = IdentityOperator(A.source) if S is not None: raise NotImplementedError if R is not None: Rc = spla.cholesky(R) # R = Rc^T * Rc Rci = spla.solve_triangular(Rc, np.eye( Rc.shape[0])) # R^{-1} = Rci * Rci^T if not trans: C = C.lincomb(Rci.T) # C <- Rci^T * C = (C^T * Rci)^T else: B = B.lincomb(Rci.T) # B <- B * Rci if not trans: B, C = C, B Z = A.source.empty(reserve=len(C) * options['maxiter']) Y = np.empty((0, 0)) K = A.source.zeros(len(B)) RF = C.copy() j = 0 j_shift = 0 shifts = init_shifts(A, E, B, C, shift_options) res = np.linalg.norm(RF.gramian(), ord=2) init_res = res Ctol = res * options['tol'] while res > Ctol and j < options['maxiter']: if not trans: AsE = A + shifts[j_shift] * E else: AsE = A + np.conj(shifts[j_shift]) * E if j == 0: if not trans: V = AsE.apply_inverse(RF) * np.sqrt(-2 * shifts[j_shift].real) else: V = AsE.apply_inverse_adjoint(RF) * np.sqrt( -2 * shifts[j_shift].real) else: if not trans: LN = AsE.apply_inverse(cat_arrays([RF, K])) else: LN = AsE.apply_inverse_adjoint(cat_arrays([RF, K])) L = LN[:len(RF)] N = LN[-len(K):] ImBN = np.eye(len(K)) - B.dot(N) ImBNKL = spla.solve(ImBN, B.dot(L)) V = (L + N.lincomb(ImBNKL.T)) * np.sqrt(-2 * shifts[j_shift].real) if np.imag(shifts[j_shift]) == 0: Z.append(V) VB = V.dot(B) Yt = np.eye(len(C)) - (VB @ VB.T) / (2 * shifts[j_shift].real) Y = spla.block_diag(Y, Yt) if not trans: EVYt = E.apply(V).lincomb(np.linalg.inv(Yt)) else: EVYt = E.apply_adjoint(V).lincomb(np.linalg.inv(Yt)) RF.axpy(np.sqrt(-2 * shifts[j_shift].real), EVYt) K += EVYt.lincomb(VB.T) j += 1 else: Z.append(V.real) Z.append(V.imag) Vr = V.real.dot(B) Vi = V.imag.dot(B) sa = np.abs(shifts[j_shift]) F1 = np.vstack((-shifts[j_shift].real / sa * Vr - shifts[j_shift].imag / sa * Vi, shifts[j_shift].imag / sa * Vr - shifts[j_shift].real / sa * Vi)) F2 = np.vstack((Vr, Vi)) F3 = np.vstack((shifts[j_shift].imag / sa * np.eye(len(C)), shifts[j_shift].real / sa * np.eye(len(C)))) Yt = spla.block_diag(np.eye(len(C)), 0.5 * np.eye(len(C))) \ - (F1 @ F1.T) / (4 * shifts[j_shift].real) \ - (F2 @ F2.T) / (4 * shifts[j_shift].real) \ - (F3 @ F3.T) / 2 Y = spla.block_diag(Y, Yt) EVYt = E.apply(cat_arrays([V.real, V.imag])).lincomb(np.linalg.inv(Yt)) RF.axpy(np.sqrt(-2 * shifts[j_shift].real), EVYt[:len(C)]) K += EVYt.lincomb(F2.T) j += 2 j_shift += 1 res = np.linalg.norm(RF.gramian(), ord=2) logger.info(f'Relative residual at step {j}: {res/init_res:.5e}') if j_shift >= shifts.size: shifts = iteration_shifts(A, E, B, RF, K, Z, shift_options) j_shift = 0 # transform solution to lrcf cf = spla.cholesky(Y) Z_cf = Z.lincomb(spla.solve_triangular(cf, np.eye(len(Z))).T) return Z_cf
def solve_sylv_schur(A, Ar, E=None, Er=None, B=None, Br=None, C=None, Cr=None): r"""Solve Sylvester equation by Schur decomposition. Solves Sylvester equation .. math:: A V E_r^T + E V A_r^T + B B_r^T = 0 or .. math:: A^T W E_r + E^T W A_r + C^T C_r = 0 or both using (generalized) Schur decomposition (Algorithms 3 and 4 in [BKS11]_), if the necessary parameters are given. Parameters ---------- A Real |Operator|. Ar Real |Operator|. It is converted into a |NumPy array| using :func:`~pymor.algorithms.to_matrix.to_matrix`. E Real |Operator| or `None` (then assumed to be the identity). Er Real |Operator| or `None` (then assumed to be the identity). It is converted into a |NumPy array| using :func:`~pymor.algorithms.to_matrix.to_matrix`. B Real |Operator| or `None`. Br Real |Operator| or `None`. It is assumed that `Br.range.from_numpy` is implemented. C Real |Operator| or `None`. Cr Real |Operator| or `None`. It is assumed that `Cr.source.from_numpy` is implemented. Returns ------- V Returned if `B` and `Br` are given, |VectorArray| from `A.source`. W Returned if `C` and `Cr` are given, |VectorArray| from `A.source`. Raises ------ ValueError If `V` and `W` cannot be returned. """ # check types assert isinstance(A, OperatorInterface) and A.linear and A.source == A.range assert isinstance( Ar, OperatorInterface) and Ar.linear and Ar.source == Ar.range assert E is None or isinstance( E, OperatorInterface) and E.linear and E.source == E.range == A.source if E is None: E = IdentityOperator(A.source) assert Er is None or isinstance( Er, OperatorInterface) and Er.linear and Er.source == Er.range == Ar.source compute_V = B is not None and Br is not None compute_W = C is not None and Cr is not None if not compute_V and not compute_W: raise ValueError( 'Not enough parameters are given to solve a Sylvester equation.') if compute_V: assert isinstance( B, OperatorInterface) and B.linear and B.range == A.source assert isinstance( Br, OperatorInterface) and Br.linear and Br.range == Ar.source assert B.source == Br.source if compute_W: assert isinstance( C, OperatorInterface) and C.linear and C.source == A.source assert isinstance( Cr, OperatorInterface) and Cr.linear and Cr.source == Ar.source assert C.range == Cr.range # convert reduced operators Ar = to_matrix(Ar, format='dense') r = Ar.shape[0] if Er is not None: Er = to_matrix(Er, format='dense') # (Generalized) Schur decomposition if Er is None: TAr, Z = spla.schur(Ar, output='complex') Q = Z else: TAr, TEr, Q, Z = spla.qz(Ar, Er, output='complex') # solve for V, from the last column to the first if compute_V: V = A.source.empty(reserve=r) BrTQ = Br.apply_adjoint(Br.range.from_numpy(Q.T)) BBrTQ = B.apply(BrTQ) for i in range(-1, -r - 1, -1): rhs = -BBrTQ[i].copy() if i < -1: if Er is not None: rhs -= A.apply(V.lincomb(TEr[i, :i:-1].conjugate())) rhs -= E.apply(V.lincomb(TAr[i, :i:-1].conjugate())) TErii = 1 if Er is None else TEr[i, i] eAaE = TErii.conjugate() * A + TAr[i, i].conjugate() * E V.append(eAaE.apply_inverse(rhs)) V = V.lincomb(Z.conjugate()[:, ::-1]) V = V.real # solve for W, from the first column to the last if compute_W: W = A.source.empty(reserve=r) CrZ = Cr.apply(Cr.source.from_numpy(Z.T)) CTCrZ = C.apply_adjoint(CrZ) for i in range(r): rhs = -CTCrZ[i].copy() if i > 0: if Er is not None: rhs -= A.apply_adjoint(W.lincomb(TEr[:i, i])) rhs -= E.apply_adjoint(W.lincomb(TAr[:i, i])) TErii = 1 if Er is None else TEr[i, i] eAaE = TErii.conjugate() * A + TAr[i, i].conjugate() * E W.append(eAaE.apply_inverse_adjoint(rhs)) W = W.lincomb(Q.conjugate()) W = W.real if compute_V and compute_W: return V, W elif compute_V: return V else: return W
def samdp(A, E, B, C, nwanted, init_shifts=None, which='LR', tol=1e-10, imagtol=1e-6, conjtol=1e-8, dorqitol=1e-4, rqitol=1e-10, maxrestart=100, krestart=20, rqi_maxiter=10, seed=0): """Compute the dominant pole triplets and residues of the transfer function of an LTI system. This function uses the subspace accelerated dominant pole (SAMDP) algorithm as described in [RM06]_ in Algorithm 2 in order to compute dominant pole triplets and residues of the transfer function .. math:: H(s) = C (s E - A)^{-1} B of an LTI system. It is possible to take advantage of prior knowledge about the poles by specifying shift parameters, which are injected after a new pole has been found. Parameters ---------- A The |Operator| A. E The |Operator| E or `None`. B The operator B as a |VectorArray| from `A.source`. C The operator C as a |VectorArray| from `A.source`. nwanted The number of dominant poles that should be computed. init_shifts A |NumPy array| containing shifts which are injected after a new pole has been found. which A string specifying the strategy by which the dominant poles and residues are selected. Possible values are: - `'LR'`: select poles with largest norm(residual) / abs(Re(pole)) - `'LS'`: select poles with largest norm(residual) / abs(pole) - `'LM'`: select poles with largest norm(residual) tol Tolerance for the residual of the poles. imagtol Relative tolerance for imaginary parts of pairs of complex conjugate eigenvalues. conjtol Tolerance for the residual of the complex conjugate of a pole. dorqitol If the residual is smaller than dorqitol the two-sided Rayleigh quotient iteration is executed. rqitol Tolerance for the relative change of a pole in the two-sided Rayleigh quotient iteration. maxrestart The maximum number of restarts. krestart Maximum dimension of search space before performing a restart. rqi_maxiter Maximum number of iterations for the two-sided Rayleigh quotient iteration. seed Random seed which is used for computing the initial shift and random restarts. Returns ------- poles A 1D |NumPy array| containing the computed dominant poles. residues A 3D |NumPy array| of shape `(len(poles), len(C), len(B))` containing the computed residues. rightev A |VectorArray| containing the right eigenvectors of the computed poles. leftev A |VectorArray| containing the left eigenvectors of the computed poles. """ logger = getLogger('pymor.algorithms.samdp.samdp') if E is None: E = IdentityOperator(A.source) assert isinstance(A, Operator) and A.linear assert not A.parametric assert A.source == A.range if E is not None: assert isinstance(E, Operator) and E.linear assert not E.parametric assert E.source == E.range assert E.source == A.source assert B in A.source assert C in A.source B_defl = B.copy() C_defl = C.copy() k = 0 nrestart = 0 nr_converged = 0 np.random.seed(seed) X = A.source.empty() Q = A.source.empty() Qt = A.source.empty() Qs = A.source.empty() Qts = A.source.empty() AX = A.source.empty() V = A.source.empty() H = np.empty((0, 1)) G = np.empty((0, 1)) poles = np.empty(0) if init_shifts is None: st = np.random.uniform() * 10.j shift_nr = 0 nr_shifts = 0 else: st = init_shifts[0] shift_nr = 1 nr_shifts = len(init_shifts) shifts = init_shifts while nrestart < maxrestart: k += 1 sEmA = st * E - A sEmAB = sEmA.apply_inverse(B_defl) Hs = C_defl.inner(sEmAB) y_all, _, u_all = spla.svd(Hs) u = u_all.conj()[0] y = y_all[:, 0] x = sEmAB.lincomb(u) v = sEmA.apply_inverse_adjoint(C_defl.lincomb(y.T)) X.append(x) V.append(v) gram_schmidt(V, atol=0, rtol=0, copy=False) gram_schmidt(X, atol=0, rtol=0, copy=False) AX.append(A.apply(X[k - 1])) if k > 1: H = np.hstack((H, V[0:k - 1].inner(AX[k - 1]))) H = np.vstack((H, V[k - 1].inner(AX))) EX = E.apply(X) if k > 1: G = np.hstack((G, V[0:k - 1].inner(EX[k - 1]))) G = np.vstack((G, V[k - 1].inner(EX))) SH, UR, URt, res = _select_max_eig(H, G, X, V, B_defl, C_defl, which) if np.all(res < np.finfo(float).eps): st = np.random.uniform() * 10.j found = False else: found = True do_rqi = True while found: theta = SH[0, 0] schurvec = X.lincomb(UR[:, 0]) schurvec.scal(1 / schurvec.norm()) lschurvec = V.lincomb(URt[:, 0]) lschurvec.scal(1 / lschurvec.norm()) st = theta nres = (A.apply(schurvec) - (E.apply(schurvec) * theta)).norm()[0] logger.info(f'Step: {k}, Theta: {theta:.5e}, Residual: {nres:.5e}') if nres < dorqitol and do_rqi: schurvec, lschurvec, theta, nres = _twosided_rqi( A, E, schurvec, lschurvec, theta, nres, imagtol, rqitol, rqi_maxiter) do_rqi = False if np.abs(np.imag(theta)) / np.abs(theta) < imagtol: rres = A.apply(schurvec.real) - E.apply( schurvec.real) * np.real(theta) nrr = rres.norm() / np.abs(np.real(theta)) if nrr - nres < np.finfo(float).eps: schurvec = schurvec.real lschurvec = lschurvec.real theta = np.real(theta) nres = nrr if nres >= tol: logger.warning( 'Two-sided RQI did not reach desired tolerance.') elif np.abs(np.imag(theta)) / np.abs(theta) < imagtol: rres = A.apply( schurvec.real) - E.apply(schurvec.real) * np.real(theta) nrr = rres.norm() / np.abs(np.real(theta)) if nrr - nres < np.finfo(float).eps: schurvec = schurvec.real lschurvec = lschurvec.real theta = np.real(theta) nres = nrr found = nr_converged < nwanted and nres < tol if found: poles = np.append(poles, theta) logger.info(f'Pole: {theta:.5e}') Q.append(schurvec) Qt.append(lschurvec) Esch = E.apply(schurvec) Qs.append(Esch) Qts.append(E.apply_adjoint(lschurvec)) nqqt = lschurvec.inner(Esch)[0][0] Q[-1].scal(1 / nqqt) Qs[-1].scal(1 / nqqt) nr_converged += 1 if k > 1: X = X.lincomb(UR[:, 1:k].T) V = V.lincomb(URt[:, 1:k].T) else: X = A.source.empty() V = A.source.empty() if np.abs(np.imag(theta)) / np.abs(theta) < imagtol: gram_schmidt(V, atol=0, rtol=0, copy=False) gram_schmidt(X, atol=0, rtol=0, copy=False) B_defl -= E.apply(Q[-1].lincomb(Qt[-1].inner(B_defl).T)) C_defl -= E.apply_adjoint(Qt[-1].lincomb( Q[-1].inner(C_defl).T)) k -= 1 cce = theta.conj() if np.abs(np.imag(cce)) / np.abs(cce) >= imagtol: ccv = schurvec.conj() ccv.scal(1 / ccv.norm()) r = A.apply(ccv) - E.apply(ccv) * cce if r.norm() / np.abs(cce) < conjtol: logger.info(f'Conjugate Pole: {cce:.5e}') poles = np.append(poles, cce) Q.append(ccv) ccvt = lschurvec.conj() Qt.append(ccvt) Esch = E.apply(ccv) Qs.append(Esch) Qts.append(E.apply_adjoint(ccvt)) nqqt = ccvt.inner(E.apply(ccv))[0][0] Q[-1].scal(1 / nqqt) Qs[-1].scal(1 / nqqt) gram_schmidt(V, atol=0, rtol=0, copy=False) gram_schmidt(X, atol=0, rtol=0, copy=False) B_defl -= E.apply(Q[-1].lincomb( Qt[-1].inner(B_defl).T)) C_defl -= E.apply_adjoint(Qt[-1].lincomb( Q[-1].inner(C_defl).T)) AX = A.apply(X) if k > 0: G = V.inner(E.apply(X)) H = V.inner(AX) SH, UR, URt, residues = _select_max_eig( H, G, X, V, B_defl, C_defl, which) found = np.any(res >= np.finfo(float).eps) else: G = np.empty((0, 1)) H = np.empty((0, 1)) found = False if nr_converged < nwanted: if found: st = SH[0, 0] else: st = np.random.uniform() * 10.j if shift_nr < nr_shifts: st = shifts[shift_nr] shift_nr += 1 elif k >= krestart: logger.info('Perform restart...') EX = E.apply(X) RR = AX.lincomb(UR.T) - EX.lincomb(UR.T).lincomb(SH.T) minidx = RR.norm().argmin() k = 1 X = X.lincomb(UR[:, minidx]) V = V.lincomb(URt[:, minidx]) gram_schmidt(V, atol=0, rtol=0, copy=False) gram_schmidt(X, atol=0, rtol=0, copy=False) G = V.inner(E.apply(X)) AX = A.apply(X) H = V.inner(AX) nrestart += 1 if k >= krestart: logger.info('Perform restart...') EX = E.apply(X) RR = AX.lincomb(UR.T) - EX.lincomb(UR.T).lincomb(SH.T) minidx = RR.norm().argmin() k = 1 X = X.lincomb(UR[:, minidx]) V = V.lincomb(URt[:, minidx]) gram_schmidt(V, atol=0, rtol=0, copy=False) gram_schmidt(X, atol=0, rtol=0, copy=False) G = V.inner(E.apply(X)) AX = A.apply(X) H = V.inner(AX) nrestart += 1 if nr_converged == nwanted or nrestart == maxrestart: rightev = Q leftev = Qt absres = np.empty(len(poles)) residues = [] for i in range(len(poles)): leftev[i].scal(1 / leftev[i].inner(E.apply(rightev[i]))[0][0]) residues.append(C.inner(rightev[i]) @ leftev[i].inner(B)) absres[i] = spla.norm(residues[-1], ord=2) residues = np.array(residues) if which == 'LR': idx = np.argsort(-absres / np.abs(np.real(poles))) elif which == 'LS': idx = np.argsort(-absres / np.abs(poles)) elif which == 'LM': idx = np.argsort(-absres) else: raise ValueError('Unknown SAMDP selection strategy.') residues = residues[idx] poles = poles[idx] rightev = rightev[idx] leftev = leftev[idx] if nr_converged < nwanted: logger.warning( 'The specified number of poles could not be computed.') break return poles, residues, rightev, leftev
def solve_sylv_schur(A, Ar, E=None, Er=None, B=None, Br=None, C=None, Cr=None): r"""Solve Sylvester equation by Schur decomposition. Solves Sylvester equation .. math:: A V E_r^T + E V A_r^T + B B_r^T = 0 or .. math:: A^T W E_r + E^T W A_r + C^T C_r = 0 or both using (generalized) Schur decomposition (Algorithms 3 and 4 in [BKS11]_), if the necessary parameters are given. Parameters ---------- A Real |Operator|. Ar Real |Operator|. It is converted into a |NumPy array| using :func:`~pymor.algorithms.to_matrix.to_matrix`. E Real |Operator| or `None` (then assumed to be the identity). Er Real |Operator| or `None` (then assumed to be the identity). It is converted into a |NumPy array| using :func:`~pymor.algorithms.to_matrix.to_matrix`. B Real |Operator| or `None`. Br Real |Operator| or `None`. It is assumed that `Br.range.from_numpy` is implemented. C Real |Operator| or `None`. Cr Real |Operator| or `None`. It is assumed that `Cr.source.from_numpy` is implemented. Returns ------- V Returned if `B` and `Br` are given, |VectorArray| from `A.source`. W Returned if `C` and `Cr` are given, |VectorArray| from `A.source`. Raises ------ ValueError If `V` and `W` cannot be returned. """ # check types assert isinstance(A, OperatorInterface) and A.linear and A.source == A.range assert isinstance(Ar, OperatorInterface) and Ar.linear and Ar.source == Ar.range assert E is None or isinstance(E, OperatorInterface) and E.linear and E.source == E.range == A.source if E is None: E = IdentityOperator(A.source) assert Er is None or isinstance(Er, OperatorInterface) and Er.linear and Er.source == Er.range == Ar.source compute_V = B is not None and Br is not None compute_W = C is not None and Cr is not None if not compute_V and not compute_W: raise ValueError('Not enough parameters are given to solve a Sylvester equation.') if compute_V: assert isinstance(B, OperatorInterface) and B.linear and B.range == A.source assert isinstance(Br, OperatorInterface) and Br.linear and Br.range == Ar.source assert B.source == Br.source if compute_W: assert isinstance(C, OperatorInterface) and C.linear and C.source == A.source assert isinstance(Cr, OperatorInterface) and Cr.linear and Cr.source == Ar.source assert C.range == Cr.range # convert reduced operators Ar = to_matrix(Ar, format='dense') r = Ar.shape[0] if Er is not None: Er = to_matrix(Er, format='dense') # (Generalized) Schur decomposition if Er is None: TAr, Z = spla.schur(Ar, output='complex') Q = Z else: TAr, TEr, Q, Z = spla.qz(Ar, Er, output='complex') # solve for V, from the last column to the first if compute_V: V = A.source.empty(reserve=r) BrTQ = Br.apply_adjoint(Br.range.from_numpy(Q.T)) BBrTQ = B.apply(BrTQ) for i in range(-1, -r - 1, -1): rhs = -BBrTQ[i].copy() if i < -1: if Er is not None: rhs -= A.apply(V.lincomb(TEr[i, :i:-1].conjugate())) rhs -= E.apply(V.lincomb(TAr[i, :i:-1].conjugate())) TErii = 1 if Er is None else TEr[i, i] eAaE = TErii.conjugate() * A + TAr[i, i].conjugate() * E V.append(eAaE.apply_inverse(rhs)) V = V.lincomb(Z.conjugate()[:, ::-1]) V = V.real # solve for W, from the first column to the last if compute_W: W = A.source.empty(reserve=r) CrZ = Cr.apply(Cr.source.from_numpy(Z.T)) CTCrZ = C.apply_adjoint(CrZ) for i in range(r): rhs = -CTCrZ[i].copy() if i > 0: if Er is not None: rhs -= A.apply_adjoint(W.lincomb(TEr[:i, i])) rhs -= E.apply_adjoint(W.lincomb(TAr[:i, i])) TErii = 1 if Er is None else TEr[i, i] eAaE = TErii.conjugate() * A + TAr[i, i].conjugate() * E W.append(eAaE.apply_inverse_adjoint(rhs)) W = W.lincomb(Q.conjugate()) W = W.real if compute_V and compute_W: return V, W elif compute_V: return V else: return W
def solve_lyap_lrcf(A, E, B, trans=False, options=None): """Compute an approximate low-rank solution of a Lyapunov equation. See :func:`pymor.algorithms.lyapunov.solve_lyap_lrcf` for a general description. This function uses the low-rank ADI iteration as described in Algorithm 4.3 in [PK16]_. Parameters ---------- A The |Operator| A. E The |Operator| E or `None`. B The operator B as a |VectorArray| from `A.source`. trans Whether the first |Operator| in the Lyapunov equation is transposed. options The solver options to use (see :func:`lyap_lrcf_solver_options`). Returns ------- Z Low-rank Cholesky factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ _solve_lyap_lrcf_check_args(A, E, B, trans) options = _parse_options(options, lyap_lrcf_solver_options(), 'lradi', None, False) logger = getLogger('pymor.algorithms.lradi.solve_lyap_lrcf') shift_options = options['shift_options'][options['shifts']] if shift_options['type'] == 'projection_shifts': init_shifts = projection_shifts_init iteration_shifts = projection_shifts else: raise ValueError('Unknown lradi shift strategy.') if E is None: E = IdentityOperator(A.source) Z = A.source.empty(reserve=len(B) * options['maxiter']) W = B.copy() j = 0 j_shift = 0 shifts = init_shifts(A, E, W, shift_options) res = np.linalg.norm(W.gramian(), ord=2) init_res = res Btol = res * options['tol'] while res > Btol and j < options['maxiter']: if shifts[j_shift].imag == 0: AaE = A + shifts[j_shift].real * E if not trans: V = AaE.apply_inverse(W) W -= E.apply(V) * (2 * shifts[j_shift].real) else: V = AaE.apply_inverse_adjoint(W) W -= E.apply_adjoint(V) * (2 * shifts[j_shift].real) Z.append(V * np.sqrt(-2 * shifts[j_shift].real)) j += 1 else: AaE = A + shifts[j_shift] * E gs = -4 * shifts[j_shift].real d = shifts[j_shift].real / shifts[j_shift].imag if not trans: V = AaE.apply_inverse(W) W += E.apply(V.real + V.imag * d) * gs else: V = AaE.apply_inverse_adjoint(W).conj() W += E.apply_adjoint(V.real + V.imag * d) * gs g = np.sqrt(gs) Z.append((V.real + V.imag * d) * g) Z.append(V.imag * (g * np.sqrt(d**2 + 1))) j += 2 j_shift += 1 res = np.linalg.norm(W.gramian(), ord=2) logger.info(f'Relative residual at step {j}: {res/init_res:.5e}') if j_shift >= shifts.size: shifts = iteration_shifts(A, E, V, shifts) j_shift = 0 if res > Btol: logger.warning(f'Prescribed relative residual tolerance was not achieved ' f'({res/init_res:e} > {options["tol"]:e}) after ' f'{options["maxiter"]} ADI steps.') return Z