def test_to_matrix(): np.random.seed(0) A = np.random.randn(2, 2) B = np.random.randn(3, 3) C = np.random.randn(3, 3) X = np.bmat([[np.eye(2) + A, np.zeros((2, 3))], [np.zeros((3, 2)), B.dot(C.T)]]) C = sps.csc_matrix(C) Aop = NumpyMatrixOperator(A) Bop = NumpyMatrixOperator(B) Cop = NumpyMatrixOperator(C) Xop = BlockDiagonalOperator([LincombOperator([IdentityOperator(NumpyVectorSpace(2)), Aop], [1, 1]), Concatenation(Bop, AdjointOperator(Cop))]) assert np.allclose(X, to_matrix(Xop)) assert np.allclose(X, to_matrix(Xop, format='csr').toarray()) np.random.seed(0) V = np.random.randn(10, 2) Vva = NumpyVectorSpace.make_array(V.T) Vop = VectorArrayOperator(Vva) assert np.allclose(V, to_matrix(Vop)) Vop = VectorArrayOperator(Vva, transposed=True) assert np.allclose(V, to_matrix(Vop).T)
def solve_ricc_lrcf(A, E, B, C, R=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 uses `scipy.linalg.solve_continuous_are`, which is a dense solver. Therefore, we assume all |Operators| and |VectorArrays| can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and :func:`~pymor.vectorarrays.interface.VectorArray.to_numpy`. Parameters ---------- A The non-parametric |Operator| A. E The non-parametric |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`. 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, R, trans) options = _parse_options(options, ricc_lrcf_solver_options(), 'scipy', None, False) if options['type'] != 'scipy': raise ValueError( f"Unexpected Riccati equation solver ({options['type']}).") A_source = A.source A = to_matrix(A, format='dense') E = to_matrix(E, format='dense') if E else None B = B.to_numpy().T C = C.to_numpy() if R is None: R = np.eye(C.shape[0] if not trans else B.shape[1]) if not trans: if E is not None: E = E.T X = solve_continuous_are(A.T, C.T, B.dot(B.T), R, E) else: X = solve_continuous_are(A, B, C.T.dot(C), R, E) return A_source.from_numpy(_chol(X).T)
def test_to_matrix(): np.random.seed(0) A = np.random.randn(2, 2) B = np.random.randn(3, 3) C = np.random.randn(3, 3) X = np.bmat([[np.eye(2) + A, np.zeros((2, 3))], [np.zeros((3, 2)), B.dot(C.T)]]) C = sps.csc_matrix(C) Aop = NumpyMatrixOperator(A) Bop = NumpyMatrixOperator(B) Cop = NumpyMatrixOperator(C) Xop = BlockDiagonalOperator([ LincombOperator([IdentityOperator(NumpyVectorSpace(2)), Aop], [1, 1]), Concatenation(Bop, AdjointOperator(Cop)) ]) assert np.allclose(X, to_matrix(Xop)) assert np.allclose(X, to_matrix(Xop, format='csr').toarray()) np.random.seed(0) V = np.random.randn(10, 2) Vva = NumpyVectorArray(V.T) Vop = VectorArrayOperator(Vva) assert np.allclose(V, to_matrix(Vop)) Vop = VectorArrayOperator(Vva, transposed=True) assert np.allclose(V, to_matrix(Vop).T)
def _lti_to_poles_b_c(rom): """Compute poles and residues. Parameters ---------- rom Reduced |LTIModel| (consisting of |NumpyMatrixOperators|). Returns ------- poles 1D |NumPy array| of poles. b |VectorArray| from `rom.B.source`. c |VectorArray| from `rom.C.range`. """ A = to_matrix(rom.A, format='dense') B = to_matrix(rom.B, format='dense') C = to_matrix(rom.C, format='dense') if isinstance(rom.E, IdentityOperator): poles, X = spla.eig(A) EX = X else: E = to_matrix(rom.E, format='dense') poles, X = spla.eig(A, E) EX = E @ X b = rom.B.source.from_numpy(spla.solve(EX, B)) c = rom.C.range.from_numpy((C @ X).T) return poles, b, c
def _lti_to_poles_b_c(rom): """Compute poles and residues. Parameters ---------- rom Reduced |LTIModel| (consisting of |NumpyMatrixOperators|). Returns ------- poles 1D |NumPy array| of poles. b |NumPy array| of shape `(rom.order, rom.dim_input)`. c |NumPy array| of shape `(rom.order, rom.dim_output)`. """ A = to_matrix(rom.A, format='dense') B = to_matrix(rom.B, format='dense') C = to_matrix(rom.C, format='dense') if isinstance(rom.E, IdentityOperator): poles, X = spla.eig(A) EX = X else: E = to_matrix(rom.E, format='dense') poles, X = spla.eig(A, E) EX = E @ X b = spla.solve(EX, B) c = (C @ X).T return poles, b, c
def get_gap_rom(rom): """Based on a rom, create model which is used to evaluate H2-Gap norm.""" A = to_matrix(rom.A, format='dense') B = to_matrix(rom.B, format='dense') C = to_matrix(rom.C, format='dense') if isinstance(rom.E, IdentityOperator): P = spla.solve_continuous_are(A.T, C.T, B.dot(B.T), np.eye(len(C)), balanced=False) F = P @ C.T else: E = to_matrix(rom.E, format='dense') P = spla.solve_continuous_are(A.T, C.T, B.dot(B.T), np.eye(len(C)), e=E.T, balanced=False) F = E @ P @ C.T AF = A - F @ C mFB = np.concatenate((-F, B), axis=1) return LTIModel.from_matrices( AF, mFB, C, E=None if isinstance(rom.E, IdentityOperator) else E)
def __init__(self, opt, A, E, B, C): super().__init__(name='RiccatiEquation', opt=opt, dim=A.source.dim) self.a = A self.e = E self.b = to_matrix(B, format='dense') self.c = to_matrix(C, format='dense') self.rhs = self.b if opt.type == pymess.MESS_OP_NONE else self.c.T self.p = []
def _poles_and_tangential_directions(rom): """Compute the poles and tangential directions of a reduced order model.""" if isinstance(rom.E, IdentityOperator): poles, Y, X = spla.eig(to_matrix(rom.A, format='dense'), left=True, right=True) else: poles, Y, X = spla.eig(to_matrix(rom.A, format='dense'), to_matrix(rom.E, format='dense'), left=True, right=True) Y = rom.B.range.make_array(Y.conj().T) X = rom.C.source.make_array(X.T) b = rom.B.apply_adjoint(Y) c = rom.C.apply(X) return poles, b, c
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 `slycot.sb03md` (if `E is None`) and `slycot.sg03ad` (if `E is not None`), which are dense solvers based on the Bartels-Stewart algorithm. Therefore, we assume A and E can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and that `B.to_numpy` is implemented. 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(), 'slycot_bartels-stewart', None, False) if options['type'] == 'slycot_bartels-stewart': X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) Z = _chol(X) else: raise ValueError( f"Unexpected Lyapunov equation solver ({options['type']}).") return A.source.from_numpy(Z.T)
def _call_pymess_dense_nm_gmpare(A, E, B, C, R, S, trans=False, options=None, plus=False): """Return the solution from pymess.dense_nm_gmpare solver.""" A = to_matrix(A, format='dense') E = to_matrix(E, format='dense') if E else None B = B.to_numpy().T C = C.to_numpy() S = S.to_numpy().T if S else None Q = B.dot(B.T) if not trans else C.T.dot(C) pymess_trans = pymess.MESS_OP_NONE if not trans else pymess.MESS_OP_TRANSPOSE if not trans: RinvC = spla.solve(R, C) if R is not None else C G = C.T.dot(RinvC) if S is not None: RinvST = spla.solve(R, S.T) if R is not None else S.T if not plus: A -= S.dot(RinvC) Q -= S.dot(RinvST) else: A += S.dot(RinvC) Q += S.dot(RinvST) else: RinvBT = spla.solve(R, B.T) if R is not None else B.T G = B.dot(RinvBT) if S is not None: RinvST = spla.solve(R, S.T) if R is not None else S.T if not plus: A -= RinvBT.T.dot(S.T) Q -= S.dot(RinvST) else: A += RinvBT.T.dot(S.T) Q += S.dot(RinvST) X, absres, relres = pymess.dense_nm_gmpare(None, A, E, Q, G, plus=plus, trans=pymess_trans, linesearch=options['linesearch'], maxit=options['maxit'], absres_tol=options['absres_tol'], relres_tol=options['relres_tol'], nrm=options['nrm']) if absres > options['absres_tol']: logger = getLogger('pymess.dense_nm_gmpcare') logger.warning(f'Desired absolute residual tolerance was not achieved ' f'({absres:e} > {options["absres_tol"]:e}).') if relres > options['relres_tol']: logger = getLogger('pymess.dense_nm_gmpcare') logger.warning(f'Desired relative residual tolerance was not achieved ' f'({relres:e} > {options["relres_tol"]:e}).') return X
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 `scipy.linalg.solve_continuous_lyapunov`, which is a dense solver for Lyapunov equations with E=I. Therefore, we assume A and E can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and that `B.to_numpy` is implemented. .. note:: If E is not `None`, the problem will be reduced to a standard continuous-time algebraic Lyapunov equation by inverting E. Parameters ---------- A The non-parametric |Operator| A. E The non-parametric |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(), 'scipy', None, False) X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) return A.source.from_numpy(_chol(X).T)
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 `slycot.sb03md` (if `E is None`) and `slycot.sg03ad` (if `E is not None`), which are dense solvers based on the Bartels-Stewart algorithm. Therefore, we assume A and E can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and that `B.to_numpy` is implemented. 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(), 'slycot_bartels-stewart', None, False) if options['type'] == 'slycot_bartels-stewart': X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) Z = _chol(X) else: raise ValueError(f"Unexpected Lyapunov equation solver ({options['type']}).") return A.source.from_numpy(Z.T)
def _poles_and_tangential_directions(rom): """Compute the poles and tangential directions of a reduced order model.""" if isinstance(rom.E, IdentityOperator): poles, Y, X = spla.eig(to_matrix(rom.A, format='dense'), left=True, right=True) else: poles, Y, X = spla.eig(to_matrix(rom.A, format='dense'), to_matrix(rom.E, format='dense'), left=True, right=True) Y = rom.B.range.make_array(Y.conj().T) X = rom.C.source.make_array(X.T) b = rom.B.apply_adjoint(Y) c = rom.C.apply(X) return poles, b, c
def assert_type_and_allclose(A, Aop, default_format): if default_format == 'dense': assert isinstance(to_matrix(Aop), np.ndarray) assert np.allclose(A, to_matrix(Aop)) elif default_format == 'sparse': assert sps.issparse(to_matrix(Aop)) assert np.allclose(A, to_matrix(Aop).toarray()) else: assert getattr(sps, 'isspmatrix_' + default_format)(to_matrix(Aop)) assert np.allclose(A, to_matrix(Aop).toarray()) assert isinstance(to_matrix(Aop, format='dense'), np.ndarray) assert np.allclose(A, to_matrix(Aop, format='dense')) assert sps.isspmatrix_csr(to_matrix(Aop, format='csr')) assert np.allclose(A, to_matrix(Aop, format='csr').toarray())
def test_expand(): ops = [NumpyMatrixOperator(np.eye(1) * i) for i in range(8)] pfs = [ProjectionParameterFunctional('p', 9, i) for i in range(8)] prods = [o * p for o, p in zip(ops, pfs)] op = ((prods[0] + prods[1] + prods[2]) @ (prods[3] + prods[4] + prods[5]) @ (prods[6] + prods[7])) eop = expand(op) assert isinstance(eop, LincombOperator) assert len(eop.operators) == 3 * 3 * 2 assert all( isinstance(o, ConcatenationOperator) and len(o.operators) == 3 for o in eop.operators) assert ({to_matrix(o)[0, 0] for o in eop.operators} == { i0 * i1 * i2 for i0, i1, i2 in product([0, 1, 2], [3, 4, 5], [6, 7]) }) assert ({ frozenset(p.index for p in pf.factors) for pf in eop.coefficients } == { frozenset([i0, i1, i2]) for i0, i1, i2 in product([0, 1, 2], [3, 4, 5], [6, 7]) })
def assert_type_and_allclose(A, Aop, default_format): if default_format == 'dense': assert isinstance(to_matrix(Aop), np.ndarray) assert np.allclose(A, to_matrix(Aop)) elif default_format == 'sparse': assert sps.issparse(to_matrix(Aop)) assert np.allclose(A, to_matrix(Aop).toarray()) else: assert getattr(sps, 'isspmatrix_' + default_format)(to_matrix(Aop)) assert np.allclose(A, to_matrix(Aop).toarray()) assert isinstance(to_matrix(Aop, format='dense'), np.ndarray) assert np.allclose(A, to_matrix(Aop, format='dense')) assert sps.isspmatrix_csr(to_matrix(Aop, format='csr')) assert np.allclose(A, to_matrix(Aop, format='csr').toarray())
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 `scipy.linalg.solve_continuous_lyapunov`, which is a dense solver for Lyapunov equations with E=I. Therefore, we assume A and E can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and that `B.to_numpy` is implemented. .. note:: If E is not `None`, the problem will be reduced to a standard continuous-time algebraic Lyapunov equation by inverting E. 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(), 'scipy', None, False) X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) return A.source.from_numpy(_chol(X).T)
def __init__(self, opt, A, E, B): super().__init__(name='LyapunovEquation', opt=opt, dim=A.source.dim) self.a = A self.e = E self.rhs = to_matrix(B, format='dense') if opt.type == pymess.MESS_OP_TRANSPOSE: self.rhs = self.rhs.T self.p = []
def test_identity_numpy_lincomb(): n = 2 space = NumpyVectorSpace(n) identity = IdentityOperator(space) numpy_operator = NumpyMatrixOperator(np.ones((n, n))) for alpha in [-1, 0, 1]: for beta in [-1, 0, 1]: idop = alpha * identity + beta * numpy_operator mat1 = alpha * np.eye(n) + beta * np.ones((n, n)) mat2 = to_matrix(idop.assemble(), format='dense') assert np.array_equal(mat1, mat2)
def test_identity_numpy_lincomb(): n = 2 space = NumpyVectorSpace(n) identity = IdentityOperator(space) numpy_operator = NumpyMatrixOperator(np.ones((n, n))) for alpha in [-1, 0, 1]: for beta in [-1, 0, 1]: idop = alpha * identity + beta * numpy_operator mat1 = alpha * np.eye(n) + beta * np.ones((n, n)) mat2 = to_matrix(idop.assemble(), format='dense') assert np.array_equal(mat1, mat2)
def solve_lyap(A, E, B, trans=False, options=None, default_solver='pymess'): """Find a factor of the solution of a Lyapunov equation. Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Lyapunov equation (if E is `None`). .. math:: A X + X A^T + B B^T = 0 or a generalized Lyapunov equation .. math:: A X E^T + E X A^T + B B^T = 0. If trans is `True`, then it solves (if E is `None`) .. math:: A^T X + X A + B^T B = 0 or .. math:: A^T X E + E^T X A + B^T B = 0. This uses the `pymess` package, in particular its `lyap` and `lradi` methods. Both methods can be used for large-scale problems. The restrictions are: - `lyap` needs access to all matrix data, i.e., it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for A, E, and B, - `lradi` needs access to the data of the operator B, i.e., it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for B. 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`). default_solver The solver to use when no `options` are specified (`'pymess'`, `'pymess_lyap'`, or `'pymess_lradi'`). Returns ------- Z Low-rank factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ _solve_lyap_check_args(A, E, B, trans) options = _parse_options(options, lyap_solver_options(), default_solver, None, False) if options['type'] == 'pymess': if A.source.dim >= PYMESS_MIN_SPARSE_SIZE: options = dict(options, type='pymess_lradi') # do not modify original dict! else: options = dict(options, type='pymess_lyap') # do not modify original dict! if options['type'] == 'pymess_lyap': A_mat = to_matrix(A, format='dense') if A.source.dim < PYMESS_MIN_SPARSE_SIZE else to_matrix(A) if E is not None: E_mat = to_matrix(E, format='dense') if A.source.dim < PYMESS_MIN_SPARSE_SIZE else to_matrix(E) else: E_mat = None B_mat = to_matrix(B, format='dense') if not trans: Z = pymess.lyap(A_mat, E_mat, B_mat) else: if E is None: Z = pymess.lyap(A_mat.T, None, B_mat.T) else: Z = pymess.lyap(A_mat.T, E_mat.T, B_mat.T) elif options['type'] == 'pymess_lradi': opts = options['opts'] if trans: opts.type = pymess.MESS_OP_TRANSPOSE else: opts.type = pymess.MESS_OP_NONE eqn = LyapunovEquation(opts, A, E, B) Z, status = pymess.lradi(eqn, opts) Z = A.source.from_numpy(np.array(Z).T) return Z
def solve_ricc(A, E=None, B=None, Q=None, C=None, R=None, G=None, trans=False, options=None, default_solver='pymess'): """Find a factor of the solution of a Riccati equation Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Riccati equation .. math:: A^T X E + E^T X A - E^T X B R^{-1} B^T X E + Q = 0. If E in `None`, it is taken to be the identity matrix. Q can instead be given as C^T * C. In this case, Q needs to be `None`, and C not `None`. B * R^{-1} B^T can instead be given by G. In this case, B and R need to be `None`, and G not `None`. If R and G are `None`, then R is taken to be the identity matrix. If trans is `True`, then the dual Riccati equation is solved .. math:: A X E^T + E X A^T - E X C^T R^{-1} C X E^T + Q = 0, where Q can be replaced by B * B^T and C^T * R^{-1} * C by G. This uses the `pymess` package, in particular its `care` and `lrnm` methods. Operators Q, R, and G are not supported, Both methods can be used for large-scale problems. The restrictions are: - `care` needs access to all matrix data, i.e., it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for A, E, B, and C, - `lrnm` needs access to the data of the operators B and C, i.e., it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for B and C. Parameters ---------- A The |Operator| A. B The |Operator| B or `None`. E The |Operator| E or `None`. Q The |Operator| Q or `None`. C The |Operator| C or `None`. R The |Operator| R or `None`. G The |Operator| G or `None`. trans If the dual equation needs to be solved. options The |solver_options| to use (see :func:`ricc_solver_options`). default_solver The solver to use when no `options` are specified (pymess, pymess_care, pymess_lrnm). Returns ------- Z Low-rank factor of the Riccati equation solution, |VectorArray| from `A.source`. """ _solve_ricc_check_args(A, E, B, Q, C, R, G, trans) options = _parse_options(options, ricc_solver_options(), default_solver, None, False) if options['type'] == 'pymess': if A.source.dim >= PYMESS_MIN_SPARSE_SIZE: options = dict(options, type='pymess_lrnm') # do not modify original dict! else: options = dict(options, type='pymess_care') # do not modify original dict! if options['type'] == 'pymess_care': if Q is not None or R is not None or G is not None: raise NotImplementedError A_mat = to_matrix(A, format='dense') if A.source.dim < PYMESS_MIN_SPARSE_SIZE else to_matrix(A) if E is not None: E_mat = to_matrix(E, format='dense') if A.source.dim < PYMESS_MIN_SPARSE_SIZE else to_matrix(E) else: E_mat = None B_mat = to_matrix(B, format='dense') if B else None C_mat = to_matrix(C, format='dense') if C else None if not trans: Z = pymess.care(A_mat, E_mat, B_mat, C_mat) else: if E is None: Z = pymess.care(A_mat.T, None, C_mat.T, B_mat.T) else: Z = pymess.care(A_mat.T, E_mat.T, C_mat.T, B_mat.T) elif options['type'] == 'pymess_lrnm': if Q is not None or R is not None or G is not None: raise NotImplementedError opts = options['opts'] if not trans: opts.type = pymess.MESS_OP_TRANSPOSE else: opts.type = pymess.MESS_OP_NONE eqn = RiccatiEquation(opts, A, E, B, C) Z, status = pymess.lrnm(eqn, opts) Z = A.source.from_numpy(np.array(Z).T) return Z
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_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 uses `slycot.sb02md` (if E and S are `None`), `slycot.sb02od` (if E is `None` and S is not `None`) and `slycot.sg03ad` (if E is not `None`), which are dense solvers. Therefore, we assume all |Operators| and |VectorArrays| can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and :func:`~pymor.vectorarrays.interfaces.VectorArrayInterface.to_numpy`. 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, R, S, trans) options = _parse_options(options, ricc_lrcf_solver_options(), 'slycot', None, False) if options['type'] != 'slycot': raise ValueError( f"Unexpected Riccati equation solver ({options['type']}).") A_source = A.source A = to_matrix(A, format='dense') E = to_matrix(E, format='dense') if E else None B = B.to_numpy().T C = C.to_numpy() S = S.to_numpy().T if S else None n = A.shape[0] dico = 'C' if E is None: if S is None: if not trans: A = A.T G = C.T.dot(C) if R is None else slycot.sb02mt( n, C.shape[0], C.T, R)[-1] else: G = B.dot(B.T) if R is None else slycot.sb02mt( n, B.shape[1], B, R)[-1] Q = B.dot(B.T) if not trans else C.T.dot(C) X, rcond = slycot.sb02md(n, A, G, Q, dico)[:2] _ricc_rcond_check('slycot.sb02md', rcond) else: m = C.shape[0] if not trans else B.shape[1] p = B.shape[1] if not trans else C.shape[0] if R is None: R = np.eye(m) if not trans: A = A.T B, C = C.T, B.T X, rcond = slycot.sb02od(n, m, A, B, C, R, dico, p=p, L=S, fact='C')[:2] _ricc_rcond_check('slycot.sb02od', rcond) else: jobb = 'B' fact = 'C' uplo = 'U' jobl = 'Z' if S is None else 'N' scal = 'N' sort = 'S' acc = 'R' m = C.shape[0] if not trans else B.shape[1] p = B.shape[1] if not trans else C.shape[0] if R is None: R = np.eye(m) if S is None: S = np.empty((n, m)) if not trans: A = A.T E = E.T B, C = C.T, B.T out = slycot.sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A, E, B, C, R, S) X = out[1] rcond = out[0] _ricc_rcond_check('slycot.sg02ad', rcond) return A_source.from_numpy(_chol(X).T)
def apply_inverse(op, V, options=None, least_squares=False, check_finite=True, default_solver='scipy_spsolve', default_least_squares_solver='scipy_least_squares_lsmr'): """Solve linear equation system. Applies the inverse of `op` to the vectors in `rhs` using PyAMG. Parameters ---------- op The linear, non-parametric |Operator| to invert. rhs |VectorArray| of right-hand sides for the equation system. options The |solver_options| to use (see :func:`solver_options`). check_finite Test if solution only containes finite values. default_solver Default solver to use (scipy_spsolve, scipy_bicgstab, scipy_bicgstab_spilu, scipy_lgmres, scipy_least_squares_lsmr, scipy_least_squares_lsqr). default_least_squares_solver Default solver to use for least squares problems (scipy_least_squares_lsmr, scipy_least_squares_lsqr). Returns ------- |VectorArray| of the solution vectors. """ assert V in op.range if isinstance(op, NumpyMatrixOperator): matrix = op._matrix else: from pymor.algorithms.to_matrix import to_matrix matrix = to_matrix(op) options = _parse_options(options, solver_options(), default_solver, default_least_squares_solver, least_squares) V = V.data promoted_type = np.promote_types(matrix.dtype, V.dtype) R = np.empty((len(V), matrix.shape[1]), dtype=promoted_type) if options['type'] == 'scipy_bicgstab': for i, VV in enumerate(V): R[i], info = bicgstab(matrix, VV, tol=options['tol'], maxiter=options['maxiter']) if info != 0: if info > 0: raise InversionError('bicgstab failed to converge after {} iterations'.format(info)) else: raise InversionError('bicgstab failed with error code {} (illegal input or breakdown)'. format(info)) elif options['type'] == 'scipy_bicgstab_spilu': if Version(scipy.version.version) >= Version('0.19'): ilu = spilu(matrix, drop_tol=options['spilu_drop_tol'], fill_factor=options['spilu_fill_factor'], drop_rule=options['spilu_drop_rule'], permc_spec=options['spilu_permc_spec']) else: if options['spilu_drop_rule']: logger = getLogger('pymor.operators.numpy._apply_inverse') logger.error("ignoring drop_rule in ilu factorization due to old SciPy") ilu = spilu(matrix, drop_tol=options['spilu_drop_tol'], fill_factor=options['spilu_fill_factor'], permc_spec=options['spilu_permc_spec']) precond = LinearOperator(matrix.shape, ilu.solve) for i, VV in enumerate(V): R[i], info = bicgstab(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], M=precond) if info != 0: if info > 0: raise InversionError('bicgstab failed to converge after {} iterations'.format(info)) else: raise InversionError('bicgstab failed with error code {} (illegal input or breakdown)'. format(info)) elif options['type'] == 'scipy_spsolve': try: # maybe remove unusable factorization: if hasattr(matrix, 'factorization'): fdtype = matrix.factorizationdtype if not np.can_cast(V.dtype, fdtype, casting='safe'): del matrix.factorization if Version(scipy.version.version) >= Version('0.14'): if hasattr(matrix, 'factorization'): # we may use a complex factorization of a real matrix to # apply it to a real vector. In that case, we downcast # the result here, removing the imaginary part, # which should be zero. R = matrix.factorization.solve(V.T).T.astype(promoted_type, copy=False) elif options['keep_factorization']: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op matrix.factorization = splu(matrix_astype_nocopy(matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) matrix.factorizationdtype = promoted_type R = matrix.factorization.solve(V.T).T else: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op R = spsolve(matrix_astype_nocopy(matrix, promoted_type), V.T, permc_spec=options['permc_spec']).T else: # see if-part for documentation if hasattr(matrix, 'factorization'): for i, VV in enumerate(V): R[i] = matrix.factorization.solve(VV).astype(promoted_type, copy=False) elif options['keep_factorization']: matrix.factorization = splu(matrix_astype_nocopy(matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) matrix.factorizationdtype = promoted_type for i, VV in enumerate(V): R[i] = matrix.factorization.solve(VV) elif len(V) > 1: factorization = splu(matrix_astype_nocopy(matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) for i, VV in enumerate(V): R[i] = factorization.solve(VV) else: R = spsolve(matrix_astype_nocopy(matrix, promoted_type), V.T, permc_spec=options['permc_spec']).reshape((1, -1)) except RuntimeError as e: raise InversionError(e) elif options['type'] == 'scipy_lgmres': for i, VV in enumerate(V): R[i], info = lgmres(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], inner_m=options['inner_m'], outer_k=options['outer_k']) if info > 0: raise InversionError('lgmres failed to converge after {} iterations'.format(info)) assert info == 0 elif options['type'] == 'scipy_least_squares_lsmr': from scipy.sparse.linalg import lsmr for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _ = lsmr(matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], maxiter=options['maxiter'], show=options['show']) assert 0 <= info <= 7 if info == 7: raise InversionError('lsmr failed to converge after {} iterations'.format(itn)) elif options['type'] == 'scipy_least_squares_lsqr': for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _, _, _ = lsqr(matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], iter_lim=options['iter_lim'], show=options['show']) assert 0 <= info <= 7 if info == 7: raise InversionError('lsmr failed to converge after {} iterations'.format(itn)) else: raise ValueError('Unknown solver type') if check_finite: if not np.isfinite(np.sum(R)): raise InversionError('Result contains non-finite values') return op.source.from_data(R)
def solve_ricc(A, E=None, B=None, Q=None, C=None, R=None, G=None, trans=False, options=None): """Find a factor of the solution of a Riccati equation Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Riccati equation .. math:: A^T X E + E^T X A - E^T X B R^{-1} B^T X E + Q = 0. If E in `None`, it is taken to be the identity matrix. Q can instead be given as C^T * C. In this case, Q needs to be `None`, and C not `None`. B * R^{-1} B^T can instead be given by G. In this case, B and R need to be `None`, and G not `None`. If R and G are `None`, then R is taken to be the identity matrix. If trans is `True`, then the dual Riccati equation is solved .. math:: A X E^T + E X A^T - E X C^T R^{-1} C X E^T + Q = 0, where Q can be replaced by B * B^T and C^T * R^{-1} * C by G. This uses the `slycot` package, in particular its interfaces to SLICOT functions `SB02MD` (for the standard Riccati equations) and `SG02AD` (for the generalized Riccati equations). These methods are only applicable to medium-sized dense problems and need access to the matrix data of all operators. Parameters ---------- A The |Operator| A. B The |Operator| B or `None`. E The |Operator| E or `None`. Q The |Operator| Q or `None`. C The |Operator| C or `None`. R The |Operator| R or `None`. G The |Operator| G or `None`. trans If the dual equation needs to be solved. options The |solver_options| to use (see :func:`ricc_solver_options`). Returns ------- Z Low-rank factor of the Riccati equation solution, |VectorArray| from `A.source`. """ _solve_ricc_check_args(A, E, B, Q, C, R, G, trans) options = _parse_options(options, ricc_solver_options(), 'slycot', None, False) assert options['type'] == 'slycot' import slycot A_mat = to_matrix(A, format='dense') B_mat = to_matrix(B, format='dense') if B else None C_mat = to_matrix(C, format='dense') if C else None R_mat = to_matrix(R, format='dense') if R else None G_mat = to_matrix(G, format='dense') if G else None Q_mat = to_matrix(Q, format='dense') if Q else None n = A_mat.shape[0] dico = 'C' if E is None: if not trans: if G is None: if R is None: G_mat = B_mat.dot(B_mat.T) else: G_mat = slycot.sb02mt(n, B_mat.shape[1], B_mat, R_mat)[-1] if C is not None: Q_mat = C_mat.T.dot(C_mat) X = slycot.sb02md(n, A_mat, G_mat, Q_mat, dico)[0] else: if G is None: if R is None: G_mat = C_mat.T.dot(C_mat) else: G_mat = slycot.sb02mt(n, C_mat.shape[0], C_mat.T, R_mat)[-1] if B is not None: Q_mat = B_mat.dot(B_mat.T) X = slycot.sb02md(n, A_mat.T, G_mat, Q_mat, dico)[0] else: E_mat = to_matrix(E, format='dense') if E else None jobb = 'B' if G is None else 'B' fact = 'C' if Q is None else 'N' uplo = 'U' jobl = 'Z' scal = 'N' sort = 'S' acc = 'R' if not trans: m = 0 if B is None else B_mat.shape[1] p = 0 if C is None else C_mat.shape[0] if G is not None: B_mat = G_mat R_mat = np.empty((1, 1)) elif R is None: R_mat = np.eye(m) if Q is None: Q_mat = C_mat L_mat = np.empty((n, m)) ret = slycot.sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A_mat, E_mat, B_mat, Q_mat, R_mat, L_mat) else: m = 0 if C is None else C_mat.shape[0] p = 0 if B is None else B_mat.shape[1] if G is not None: C_mat = G_mat R_mat = np.empty((1, 1)) elif R is None: C_mat = C_mat.T R_mat = np.eye(m) else: C_mat = C_mat.T if Q is None: Q_mat = B_mat.T L_mat = np.empty((n, m)) ret = slycot.sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A_mat.T, E_mat.T, C_mat, Q_mat, R_mat, L_mat) X = ret[1] iwarn = ret[-1] if iwarn == 1: print('slycot.sg02ad warning: solution may be inaccurate.') from pymor.bindings.scipy import chol Z = chol(X, copy=False) Z = A.source.from_numpy(np.array(Z).T) return Z
def apply_inverse(op, V, initial_guess=None, options=None, least_squares=False, check_finite=True, default_solver='scipy_spsolve', default_least_squares_solver='scipy_least_squares_lsmr'): """Solve linear equation system. Applies the inverse of `op` to the vectors in `V` using SciPy. Parameters ---------- op The linear, non-parametric |Operator| to invert. V |VectorArray| of right-hand sides for the equation system. initial_guess |VectorArray| with the same length as `V` containing initial guesses for the solution. Some implementations of `apply_inverse` may ignore this parameter. If `None` a solver-dependent default is used. options The |solver_options| to use (see :func:`solver_options`). least_squares If `True`, return least squares solution. check_finite Test if solution only contains finite values. default_solver Default solver to use (scipy_spsolve, scipy_bicgstab, scipy_bicgstab_spilu, scipy_lgmres, scipy_least_squares_lsmr, scipy_least_squares_lsqr). default_least_squares_solver Default solver to use for least squares problems (scipy_least_squares_lsmr, scipy_least_squares_lsqr). Returns ------- |VectorArray| of the solution vectors. """ assert V in op.range assert initial_guess is None or initial_guess in op.source and len( initial_guess) == len(V) if isinstance(op, NumpyMatrixOperator): matrix = op.matrix else: from pymor.algorithms.to_matrix import to_matrix matrix = to_matrix(op) options = _parse_options(options, solver_options(), default_solver, default_least_squares_solver, least_squares) V = V.to_numpy() initial_guess = initial_guess.to_numpy( ) if initial_guess is not None else None promoted_type = np.promote_types(matrix.dtype, V.dtype) R = np.empty((len(V), matrix.shape[1]), dtype=promoted_type) if options['type'] == 'scipy_bicgstab': for i, VV in enumerate(V): R[i], info = bicgstab( matrix, VV, initial_guess[i] if initial_guess is not None else None, tol=options['tol'], maxiter=options['maxiter']) if info != 0: if info > 0: raise InversionError( f'bicgstab failed to converge after {info} iterations') else: raise InversionError( 'bicgstab failed with error code {} (illegal input or breakdown)' .format(info)) elif options['type'] == 'scipy_bicgstab_spilu': ilu = spilu(matrix, drop_tol=options['spilu_drop_tol'], fill_factor=options['spilu_fill_factor'], drop_rule=options['spilu_drop_rule'], permc_spec=options['spilu_permc_spec']) precond = LinearOperator(matrix.shape, ilu.solve) for i, VV in enumerate(V): R[i], info = bicgstab( matrix, VV, initial_guess[i] if initial_guess is not None else None, tol=options['tol'], maxiter=options['maxiter'], M=precond) if info != 0: if info > 0: raise InversionError( f'bicgstab failed to converge after {info} iterations') else: raise InversionError( 'bicgstab failed with error code {} (illegal input or breakdown)' .format(info)) elif options['type'] == 'scipy_spsolve': try: # maybe remove unusable factorization: if hasattr(matrix, 'factorization'): fdtype = matrix.factorizationdtype if not np.can_cast(V.dtype, fdtype, casting='safe'): del matrix.factorization if hasattr(matrix, 'factorization'): # we may use a complex factorization of a real matrix to # apply it to a real vector. In that case, we downcast # the result here, removing the imaginary part, # which should be zero. R = matrix.factorization.solve(V.T).T.astype(promoted_type, copy=False) elif options['keep_factorization']: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op matrix.factorization = splu(matrix_astype_nocopy( matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) matrix.factorizationdtype = promoted_type R = matrix.factorization.solve(V.T).T else: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op R = spsolve(matrix_astype_nocopy(matrix, promoted_type), V.T, permc_spec=options['permc_spec']).T except RuntimeError as e: raise InversionError(e) elif options['type'] == 'scipy_lgmres': for i, VV in enumerate(V): R[i], info = lgmres( matrix, VV, initial_guess[i] if initial_guess is not None else None, tol=options['tol'], atol=options['tol'], maxiter=options['maxiter'], inner_m=options['inner_m'], outer_k=options['outer_k']) if info > 0: raise InversionError( f'lgmres failed to converge after {info} iterations') assert info == 0 elif options['type'] == 'scipy_least_squares_lsmr': from scipy.sparse.linalg import lsmr for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _ = lsmr( matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], maxiter=options['maxiter'], show=options['show'], x0=initial_guess[i] if initial_guess is not None else None) assert 0 <= info <= 7 if info == 7: raise InversionError( f'lsmr failed to converge after {itn} iterations') elif options['type'] == 'scipy_least_squares_lsqr': for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _, _, _ = lsqr( matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], iter_lim=options['iter_lim'], show=options['show'], x0=initial_guess[i] if initial_guess is not None else None) assert 0 <= info <= 7 if info == 7: raise InversionError( f'lsmr failed to converge after {itn} iterations') else: raise ValueError('Unknown solver type') if check_finite: if not np.isfinite(np.sum(R)): raise InversionError('Result contains non-finite values') return op.source.from_numpy(R)
def solve_lyap(A, E, B, trans=False, options=None): """Find a factor of the solution of a Lyapunov equation. Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Lyapunov equation (if E is `None`). .. math:: A X + X A^T + B B^T = 0 or generalized Lyapunov equation .. math:: A X E^T + E X A^T + B B^T = 0. If trans is `True`, then it solves (if E is `None`) .. math:: A^T X + X A + B^T B = 0 or .. math:: A^T X E + E^T X A + B^T B = 0. This uses the `scipy.linalg.spla.solve_continuous_lyapunov` method. It is only applicable to the standard Lyapunov equation (E = I). Furthermore, it can only solve medium-sized dense problems and assumes access to the matrix data of all operators. 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`. """ _solve_lyap_check_args(A, E, B, trans) options = _parse_options(options, lyap_solver_options(), 'scipy', None, False) assert options['type'] == 'scipy' if E is not None: raise NotImplementedError import scipy.linalg as spla A_mat = to_matrix(A, format='dense') B_mat = to_matrix(B, format='dense') if not trans: X = spla.solve_continuous_lyapunov(A_mat, -B_mat.dot(B_mat.T)) else: X = spla.solve_continuous_lyapunov(A_mat.T, -B_mat.T.dot(B_mat)) Z = chol(X, copy=False) Z = A.source.from_numpy(np.array(Z).T) return Z
def solve_ricc(A, E=None, B=None, Q=None, C=None, R=None, G=None, trans=False, options=None): """Find a factor of the solution of a Riccati equation using solve_continuous_are. Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Riccati equation .. math:: A^T X E + E^T X A - E^T X B R^{-1} B^T X E + Q = 0. If E in `None`, it is taken to be the identity matrix. Q can instead be given as C^T * C. In this case, Q needs to be `None`, and C not `None`. B * R^{-1} B^T can instead be given by G. In this case, B and R need to be `None`, and G not `None`. If R and G are `None`, then R is taken to be the identity matrix. If trans is `True`, then the dual Riccati equation is solved .. math:: A X E^T + E X A^T - E X C^T R^{-1} C X E^T + Q = 0, where Q can be replaced by B * B^T and C^T * R^{-1} * C by G. This uses the `scipy.linalg.spla.solve_continuous_are` method. Generalized Riccati equation is not supported. It can only solve medium-sized dense problems and assumes access to the matrix data of all operators. Parameters ---------- A The |Operator| A. B The |Operator| B or `None`. E The |Operator| E or `None`. Q The |Operator| Q or `None`. C The |Operator| C or `None`. R The |Operator| R or `None`. G The |Operator| G or `None`. trans If the dual equation needs to be solved. options The |solver_options| to use (see :func:`ricc_solver_options`). Returns ------- Z Low-rank factor of the Riccati equation solution, |VectorArray| from `A.source`. """ _solve_ricc_check_args(A, E, B, Q, C, R, G, trans) options = _parse_options(options, lyap_solver_options(), 'scipy', None, False) assert options['type'] == 'scipy' if E is not None or G is not None: raise NotImplementedError import scipy.linalg as spla A_mat = to_matrix(A, format='dense') B_mat = to_matrix(B, format='dense') if B else None C_mat = to_matrix(C, format='dense') if C else None Q_mat = to_matrix(Q, format='dense') if Q else None R_mat = to_matrix(R, format='dense') if R else None if R is None: if not trans: R_mat = np.eye(B.source.dim) else: R_mat = np.eye(C.range.dim) if not trans: if Q is None: Q_mat = C_mat.T.dot(C_mat) X = spla.solve_continuous_are(A_mat, B_mat, Q_mat, R_mat) else: if Q is None: Q_mat = B_mat.dot(B_mat.T) X = spla.solve_continuous_are(A_mat.T, C_mat.T, Q_mat, R_mat) Z = chol(X, copy=False) Z = A.source.from_numpy(np.array(Z).T) return Z
def apply_inverse(op, V, options=None, least_squares=False, check_finite=True, default_solver='pyamg_solve'): """Solve linear equation system. Applies the inverse of `op` to the vectors in `rhs` using PyAMG. Parameters ---------- op The linear, non-parametric |Operator| to invert. rhs |VectorArray| of right-hand sides for the equation system. options The |solver_options| to use (see :func:`solver_options`). check_finite Test if solution only containes finite values. default_solver Default solver to use (pyamg_solve, pyamg_rs, pyamg_sa). Returns ------- |VectorArray| of the solution vectors. """ assert V in op.range if isinstance(op, NumpyMatrixOperator): matrix = op._matrix else: from pymor.algorithms.to_matrix import to_matrix matrix = to_matrix(op) options = _parse_options(options, solver_options(), default_solver, None, least_squares) V = V.data promoted_type = np.promote_types(matrix.dtype, V.dtype) R = np.empty((len(V), matrix.shape[1]), dtype=promoted_type) if options['type'] == 'pyamg_solve': if len(V) > 0: V_iter = iter(enumerate(V)) R[0], ml = pyamg.solve(matrix, next(V_iter)[1], tol=options['tol'], maxiter=options['maxiter'], return_solver=True) for i, VV in V_iter: R[i] = pyamg.solve(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], existing_solver=ml) elif options['type'] == 'pyamg_rs': ml = pyamg.ruge_stuben_solver(matrix, strength=options['strength'], CF=options['CF'], presmoother=options['presmoother'], postsmoother=options['postsmoother'], max_levels=options['max_levels'], max_coarse=options['max_coarse'], coarse_solver=options['coarse_solver']) for i, VV in enumerate(V): R[i] = ml.solve(VV, tol=options['tol'], maxiter=options['maxiter'], cycle=options['cycle'], accel=options['accel']) elif options['type'] == 'pyamg_sa': ml = pyamg.smoothed_aggregation_solver(matrix, symmetry=options['symmetry'], strength=options['strength'], aggregate=options['aggregate'], smooth=options['smooth'], presmoother=options['presmoother'], postsmoother=options['postsmoother'], improve_candidates=options['improve_candidates'], max_levels=options['max_levels'], max_coarse=options['max_coarse'], diagonal_dominance=options['diagonal_dominance']) for i, VV in enumerate(V): R[i] = ml.solve(VV, tol=options['tol'], maxiter=options['maxiter'], cycle=options['cycle'], accel=options['accel']) else: raise ValueError('Unknown solver type') if check_finite: if not np.isfinite(np.sum(R)): raise InversionError('Result contains non-finite values') return op.source.from_data(R)
def apply_inverse(op, V, options=None, least_squares=False, check_finite=True, default_solver='pyamg_solve'): """Solve linear equation system. Applies the inverse of `op` to the vectors in `rhs` using PyAMG. Parameters ---------- op The linear, non-parametric |Operator| to invert. rhs |VectorArray| of right-hand sides for the equation system. options The |solver_options| to use (see :func:`solver_options`). least_squares Must be `False`. check_finite Test if solution only contains finite values. default_solver Default solver to use (pyamg_solve, pyamg_rs, pyamg_sa). Returns ------- |VectorArray| of the solution vectors. """ assert V in op.range if least_squares: raise NotImplementedError if isinstance(op, NumpyMatrixOperator): matrix = op.matrix else: from pymor.algorithms.to_matrix import to_matrix matrix = to_matrix(op) options = _parse_options(options, solver_options(), default_solver, None, least_squares) V = V.to_numpy() promoted_type = np.promote_types(matrix.dtype, V.dtype) R = np.empty((len(V), matrix.shape[1]), dtype=promoted_type) if options['type'] == 'pyamg_solve': if len(V) > 0: V_iter = iter(enumerate(V)) R[0], ml = pyamg.solve(matrix, next(V_iter)[1], tol=options['tol'], maxiter=options['maxiter'], return_solver=True) for i, VV in V_iter: R[i] = pyamg.solve(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], existing_solver=ml) elif options['type'] == 'pyamg_rs': ml = pyamg.ruge_stuben_solver( matrix, strength=options['strength'], CF=options['CF'], presmoother=options['presmoother'], postsmoother=options['postsmoother'], max_levels=options['max_levels'], max_coarse=options['max_coarse'], coarse_solver=options['coarse_solver']) for i, VV in enumerate(V): R[i] = ml.solve(VV, tol=options['tol'], maxiter=options['maxiter'], cycle=options['cycle'], accel=options['accel']) elif options['type'] == 'pyamg_sa': ml = pyamg.smoothed_aggregation_solver( matrix, symmetry=options['symmetry'], strength=options['strength'], aggregate=options['aggregate'], smooth=options['smooth'], presmoother=options['presmoother'], postsmoother=options['postsmoother'], improve_candidates=options['improve_candidates'], max_levels=options['max_levels'], max_coarse=options['max_coarse'], diagonal_dominance=options['diagonal_dominance']) for i, VV in enumerate(V): R[i] = ml.solve(VV, tol=options['tol'], maxiter=options['maxiter'], cycle=options['cycle'], accel=options['accel']) else: raise ValueError('Unknown solver type') if check_finite: if not np.isfinite(np.sum(R)): raise InversionError('Result contains non-finite values') return op.source.from_numpy(R)
def solve_lyap_lrcf(A, E, B, trans=False, options=None, default_solver=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 `pymess.glyap` and `pymess.lradi`. For both methods, :meth:`~pymor.vectorarrays.interfaces.VectorArrayInterface.to_numpy` and :meth:`~pymor.vectorarrays.interfaces.VectorSpaceInterface.from_numpy` need to be implemented for `A.source`. Additionally, since `glyap` is a dense solver, it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for A and E. If the solver is not specified using the options or default_solver arguments, `glyap` is used for small problems (smaller than defined with :func:`~pymor.algorithms.lyapunov.mat_eqn_sparse_min_size`) and `lradi` for large problems. 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`). default_solver Default solver to use (pymess_lradi, pymess_glyap). If `None`, choose solver depending on the dimension of A. Returns ------- Z Low-rank Cholesky factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ _solve_lyap_lrcf_check_args(A, E, B, trans) if default_solver is None: default_solver = 'pymess_lradi' if A.source.dim >= mat_eqn_sparse_min_size() else 'pymess_glyap' options = _parse_options(options, lyap_lrcf_solver_options(), default_solver, None, False) if options['type'] == 'pymess_glyap': X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) Z = _chol(X) elif options['type'] == 'pymess_lradi': opts = options['opts'] opts.type = pymess.MESS_OP_NONE if not trans else pymess.MESS_OP_TRANSPOSE eqn = LyapunovEquation(opts, A, E, B) Z, status = pymess.lradi(eqn, opts) else: raise ValueError(f'Unexpected Lyapunov equation solver ({options["type"]}).') return A.source.from_numpy(Z.T)
def solve_lyap(A, E, B, trans=False, options=None): """Find a factor of the solution of a Lyapunov equation. Returns factor :math:`Z` such that :math:`Z Z^T` is approximately the solution :math:`X` of a Lyapunov equation (if E is `None`). .. math:: A X + X A^T + B B^T = 0 or generalized Lyapunov equation .. math:: A X E^T + E X A^T + B B^T = 0. If trans is `True`, then it solves (if E is `None`) .. math:: A^T X + X A + B^T B = 0 or .. math:: A^T X E + E^T X A + B^T B = 0. This uses the `slycot` package, in particular its interfaces to SLICOT functions `SB03MD` (for the standard Lyapunov equations) and `SG03AD` (for the generalized Lyapunov equations). These methods are only applicable to medium-sized dense problems and need access to the matrix data of all operators. 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`. """ _solve_lyap_check_args(A, E, B, trans) options = _parse_options(options, lyap_solver_options(), 'slycot', None, False) assert options['type'] == 'slycot' import slycot A_mat = to_matrix(A, format='dense') if E is not None: E_mat = to_matrix(E, format='dense') B_mat = to_matrix(B, format='dense') n = A_mat.shape[0] if not trans: C = -B_mat.dot(B_mat.T) trana = 'T' else: C = -B_mat.T.dot(B_mat) trana = 'N' dico = 'C' if E is None: U = np.zeros((n, n)) X, scale, _, _, _ = slycot.sb03md(n, C, A_mat, U, dico, trana=trana) else: job = 'B' fact = 'N' Q = np.zeros((n, n)) Z = np.zeros((n, n)) uplo = 'L' X = C _, _, _, _, X, scale, _, _, _, _, _ = slycot.sg03ad(dico, job, fact, trana, uplo, n, A_mat, E_mat, Q, Z, X) from pymor.bindings.scipy import chol Z = chol(X, copy=False) Z = A.source.from_numpy(np.array(Z).T) return Z
def solve_lyap_lrcf(A, E, B, trans=False, options=None, default_solver=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 `pymess.glyap` and `pymess.lradi`. For both methods, :meth:`~pymor.vectorarrays.interface.VectorArray.to_numpy` and :meth:`~pymor.vectorarrays.interface.VectorSpace.from_numpy` need to be implemented for `A.source`. Additionally, since `glyap` is a dense solver, it expects :func:`~pymor.algorithms.to_matrix.to_matrix` to work for A and E. If the solver is not specified using the options or default_solver arguments, `glyap` is used for small problems (smaller than defined with :func:`~pymor.algorithms.lyapunov.mat_eqn_sparse_min_size`) and `lradi` for large problems. Parameters ---------- A The non-parametric |Operator| A. E The non-parametric |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`). default_solver Default solver to use (pymess_lradi, pymess_glyap). If `None`, choose solver depending on the dimension of A. Returns ------- Z Low-rank Cholesky factor of the Lyapunov equation solution, |VectorArray| from `A.source`. """ _solve_lyap_lrcf_check_args(A, E, B, trans) if default_solver is None: default_solver = 'pymess_lradi' if A.source.dim >= mat_eqn_sparse_min_size( ) else 'pymess_glyap' options = _parse_options(options, lyap_lrcf_solver_options(), default_solver, None, False) if options['type'] == 'pymess_glyap': X = solve_lyap_dense(to_matrix(A, format='dense'), to_matrix(E, format='dense') if E else None, B.to_numpy().T if not trans else B.to_numpy(), trans=trans, options=options) Z = _chol(X) elif options['type'] == 'pymess_lradi': opts = options['opts'] opts.type = pymess.MESS_OP_NONE if not trans else pymess.MESS_OP_TRANSPOSE eqn = LyapunovEquation(opts, A, E, B) Z, status = pymess.lradi(eqn, opts) relres = status.res2_norm / status.res2_0 if relres > opts.adi.res2_tol: logger = getLogger('pymor.bindings.pymess.solve_lyap_lrcf') logger.warning( f'Desired relative residual tolerance was not achieved ' f'({relres:e} > {opts.adi.res2_tol:e}).') else: raise ValueError( f'Unexpected Lyapunov equation solver ({options["type"]}).') return A.source.from_numpy(Z.T)
def run_cl_simulation(rom, name, Re=110, level=2, palpha=1e-3, control='bc'): """Run the closed-loop simulation with reduced LQG controller.""" # define strings, directories and paths for data storage setup_str = 'lvl_' + str(level) + ('_' + control if control is not None else '') \ + '_re_' + str(Re) + ('_palpha_' + str(palpha) if control == 'bc' else '') data_path = '../data/' + 'lvl_' + str(level) + ('_' + control if control is not None else '') setup_path = data_path + '/re_' + str(Re) + ('_palpha_' + str(palpha) if control == 'bc' else '') simulation_path = setup_str + '/' + name + '_simulation' if not os.path.exists(simulation_path): os.makedirs(simulation_path) with open(simulation_path + '/rom_' + str(rom.order) + '.csv', 'w') as file: file.write('t, yerr \n') # define first order model and matrices for simulation fom = load_fom(Re=110, level=2, palpha=1e-3, control='bc') mats = spio.loadmat(data_path + '/mats') M = mats['M'] J = mats['J'] hmat = mats['H'] fv = mats['fv'] + 1. / Re * mats['fv_diff'] + mats['fv_conv'] fp = mats['fp'] + mats['fp_div'] vcmat = mats['Cv'] NV, NP = fv.shape[0], fp.shape[0] if control == 'bc': A = 1. / Re * mats['A'] + mats['L1'] + mats[ 'L2'] + 1. / palpha * mats['Arob'] B = 1. / palpha * mats['Brob'] else: A = 1. / Re * mats['A'] + mats['L1'] + mats['L2'] B = mats['B'] # restrict to less dofs in the input NU = B.shape[1] B = B[:, [0, NU // 2]] # compute steady-state solution and linearized convection if not os.path.isfile(setup_path + '/ss_nse_sol'): ss_nse_v, _ = solve_steadystate_nse(mats, Re, control, palpha=palpha) conv_mat = linearized_convection(mats['H'], ss_nse_v) spio.savemat(setup_path + '/ss_nse_sol', { 'ss_nse_v': ss_nse_v, 'conv_mat': conv_mat }) else: ss_nse_sol = spio.loadmat(setup_path + '/ss_nse_sol') ss_nse_v, conv_mat = ss_nse_sol['ss_nse_v'], ss_nse_sol['conv_mat'] # Define parameters for time stepping t0 = 0. tE = 8. Nts = 2**12 DT = (tE - t0) / Nts trange = np.linspace(t0, tE, Nts + 1) # Define functions that represent the system inputs if control is None: def bbcu(ko_state): return np.zeros((NV, 1)) def update_ko_state(ko_state, Cv, DT): return ko_state else: Arom = to_matrix(rom.A, format='dense') # .real? Brom = to_matrix(rom.B, format='dense') Crom = to_matrix(rom.C, format='dense') XCARE = spla.solve_continuous_are(Arom, Brom, Crom.T @ Crom, np.eye(Brom.shape[1]), balanced=False) XFARE = spla.solve_continuous_are(Arom.T, Crom.T, Brom @ Brom.T, np.eye(Crom.shape[0]), balanced=False) # define control based on Kalman observer state def bbcu(ko_state): uvec = -Brom.T @ XCARE @ ko_state return B @ uvec ko1_mat = Arom - XFARE @ Crom.T @ Crom - Brom @ Brom.T @ XCARE ko2_mat = XFARE @ Crom.T lu_piv = spla.lu_factor(np.eye(rom.order) - DT * ko1_mat) Css = vcmat @ ss_nse_v # function that determines the next state of the Kalman observer via implicit euler step def update_ko_state(ko_state, Cv, DT): return spla.lu_solve(lu_piv, ko_state + DT * ko2_mat @ (Cv - Css)) # introduce small perturbation to steady-state solution as initial value pert = fom.A.source.project_onto_subspace(fom.A.operator.source.ones(), trans=True).to_numpy().T old_v = ss_nse_v + 1e-3 * pert # initialize state for observer ko_state = np.zeros((rom.order, 1)) sysmat = sps.vstack([ sps.hstack([M + DT * A, -J.T]), sps.hstack([J, sps.csc_matrix((NP, NP))]) ]).tocsc() sysmati = spsla.factorized(sysmat) try: for k, t in enumerate(trange): crhsv = M * old_v + DT * (fv - eva_quadterm(hmat, old_v) + bbcu(ko_state)) crhs = np.vstack([crhsv, fp]) vp_new = np.atleast_2d(sysmati(crhs.flatten())).T old_v = vp_new[:NV] Cv = vcmat @ old_v ko_state = update_ko_state(ko_state, Cv, DT) print(k, '/', Nts) print(spla.norm(Cv - Css, 2)) with open(simulation_path + '/rom_' + str(rom.order) + '.csv', 'a') as file: file.write(str(t) + ',' + str(spla.norm(Cv - Css, 2)) + '\n') except: with open('simulation_error_log.txt', 'a') as file: file.write(name + '_' + str(rom.order) + '\n')
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 uses `slycot.sb02md` (if E and S are `None`), `slycot.sb02od` (if E is `None` and S is not `None`) and `slycot.sg03ad` (if E is not `None`), which are dense solvers. Therefore, we assume all |Operators| and |VectorArrays| can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and :func:`~pymor.vectorarrays.interfaces.VectorArrayInterface.to_numpy`. 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, R, S, trans) options = _parse_options(options, ricc_lrcf_solver_options(), 'slycot', None, False) if options['type'] != 'slycot': raise ValueError(f"Unexpected Riccati equation solver ({options['type']}).") A_source = A.source A = to_matrix(A, format='dense') E = to_matrix(E, format='dense') if E else None B = B.to_numpy().T C = C.to_numpy() S = S.to_numpy().T if S else None n = A.shape[0] dico = 'C' if E is None: if S is None: if not trans: A = A.T G = C.T.dot(C) if R is None else slycot.sb02mt(n, C.shape[0], C.T, R)[-1] else: G = B.dot(B.T) if R is None else slycot.sb02mt(n, B.shape[1], B, R)[-1] Q = B.dot(B.T) if not trans else C.T.dot(C) X, rcond = slycot.sb02md(n, A, G, Q, dico)[:2] _ricc_rcond_check('slycot.sb02md', rcond) else: m = C.shape[0] if not trans else B.shape[1] p = B.shape[1] if not trans else C.shape[0] if R is None: R = np.eye(m) if not trans: A = A.T B, C = C.T, B.T X, rcond = slycot.sb02od(n, m, A, B, C, R, dico, p=p, L=S, fact='C')[:2] _ricc_rcond_check('slycot.sb02od', rcond) else: jobb = 'B' fact = 'C' uplo = 'U' jobl = 'Z' if S is None else 'N' scal = 'N' sort = 'S' acc = 'R' m = C.shape[0] if not trans else B.shape[1] p = B.shape[1] if not trans else C.shape[0] if R is None: R = np.eye(m) if S is None: S = np.empty((n, m)) if not trans: A = A.T E = E.T B, C = C.T, B.T out = slycot.sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A, E, B, C, R, S) X = out[1] rcond = out[0] _ricc_rcond_check('slycot.sg02ad', rcond) return A_source.from_numpy(_chol(X).T)
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 apply_inverse(op, V, options=None, least_squares=False, check_finite=True, default_solver='scipy_spsolve', default_least_squares_solver='scipy_least_squares_lsmr'): """Solve linear equation system. Applies the inverse of `op` to the vectors in `rhs` using PyAMG. Parameters ---------- op The linear, non-parametric |Operator| to invert. rhs |VectorArray| of right-hand sides for the equation system. options The |solver_options| to use (see :func:`solver_options`). check_finite Test if solution only containes finite values. default_solver Default solver to use (scipy_spsolve, scipy_bicgstab, scipy_bicgstab_spilu, scipy_lgmres, scipy_least_squares_lsmr, scipy_least_squares_lsqr). default_least_squares_solver Default solver to use for least squares problems (scipy_least_squares_lsmr, scipy_least_squares_lsqr). Returns ------- |VectorArray| of the solution vectors. """ assert V in op.range if isinstance(op, NumpyMatrixOperator): matrix = op._matrix else: from pymor.algorithms.to_matrix import to_matrix matrix = to_matrix(op) options = _parse_options(options, solver_options(), default_solver, default_least_squares_solver, least_squares) V = V.data promoted_type = np.promote_types(matrix.dtype, V.dtype) R = np.empty((len(V), matrix.shape[1]), dtype=promoted_type) if options['type'] == 'scipy_bicgstab': for i, VV in enumerate(V): R[i], info = bicgstab(matrix, VV, tol=options['tol'], maxiter=options['maxiter']) if info != 0: if info > 0: raise InversionError( 'bicgstab failed to converge after {} iterations'. format(info)) else: raise InversionError( 'bicgstab failed with error code {} (illegal input or breakdown)' .format(info)) elif options['type'] == 'scipy_bicgstab_spilu': if Version(scipy.version.version) >= Version('0.19'): ilu = spilu(matrix, drop_tol=options['spilu_drop_tol'], fill_factor=options['spilu_fill_factor'], drop_rule=options['spilu_drop_rule'], permc_spec=options['spilu_permc_spec']) else: if options['spilu_drop_rule']: logger = getLogger('pymor.operators.numpy._apply_inverse') logger.error( "ignoring drop_rule in ilu factorization due to old SciPy") ilu = spilu(matrix, drop_tol=options['spilu_drop_tol'], fill_factor=options['spilu_fill_factor'], permc_spec=options['spilu_permc_spec']) precond = LinearOperator(matrix.shape, ilu.solve) for i, VV in enumerate(V): R[i], info = bicgstab(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], M=precond) if info != 0: if info > 0: raise InversionError( 'bicgstab failed to converge after {} iterations'. format(info)) else: raise InversionError( 'bicgstab failed with error code {} (illegal input or breakdown)' .format(info)) elif options['type'] == 'scipy_spsolve': try: # maybe remove unusable factorization: if hasattr(matrix, 'factorization'): fdtype = matrix.factorizationdtype if not np.can_cast(V.dtype, fdtype, casting='safe'): del matrix.factorization if Version(scipy.version.version) >= Version('0.14'): if hasattr(matrix, 'factorization'): # we may use a complex factorization of a real matrix to # apply it to a real vector. In that case, we downcast # the result here, removing the imaginary part, # which should be zero. R = matrix.factorization.solve(V.T).T.astype(promoted_type, copy=False) elif options['keep_factorization']: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op matrix.factorization = splu( matrix_astype_nocopy(matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) matrix.factorizationdtype = promoted_type R = matrix.factorization.solve(V.T).T else: # the matrix is always converted to the promoted type. # if matrix.dtype == promoted_type, this is a no_op R = spsolve(matrix_astype_nocopy(matrix, promoted_type), V.T, permc_spec=options['permc_spec']).T else: # see if-part for documentation if hasattr(matrix, 'factorization'): for i, VV in enumerate(V): R[i] = matrix.factorization.solve(VV).astype( promoted_type, copy=False) elif options['keep_factorization']: matrix.factorization = splu( matrix_astype_nocopy(matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) matrix.factorizationdtype = promoted_type for i, VV in enumerate(V): R[i] = matrix.factorization.solve(VV) elif len(V) > 1: factorization = splu(matrix_astype_nocopy( matrix.tocsc(), promoted_type), permc_spec=options['permc_spec']) for i, VV in enumerate(V): R[i] = factorization.solve(VV) else: R = spsolve(matrix_astype_nocopy(matrix, promoted_type), V.T, permc_spec=options['permc_spec']).reshape( (1, -1)) except RuntimeError as e: raise InversionError(e) elif options['type'] == 'scipy_lgmres': for i, VV in enumerate(V): R[i], info = lgmres(matrix, VV, tol=options['tol'], maxiter=options['maxiter'], inner_m=options['inner_m'], outer_k=options['outer_k']) if info > 0: raise InversionError( 'lgmres failed to converge after {} iterations'.format( info)) assert info == 0 elif options['type'] == 'scipy_least_squares_lsmr': from scipy.sparse.linalg import lsmr for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _ = lsmr(matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], maxiter=options['maxiter'], show=options['show']) assert 0 <= info <= 7 if info == 7: raise InversionError( 'lsmr failed to converge after {} iterations'.format(itn)) elif options['type'] == 'scipy_least_squares_lsqr': for i, VV in enumerate(V): R[i], info, itn, _, _, _, _, _, _, _ = lsqr( matrix, VV, damp=options['damp'], atol=options['atol'], btol=options['btol'], conlim=options['conlim'], iter_lim=options['iter_lim'], show=options['show']) assert 0 <= info <= 7 if info == 7: raise InversionError( 'lsmr failed to converge after {} iterations'.format(itn)) else: raise ValueError('Unknown solver type') if check_finite: if not np.isfinite(np.sum(R)): raise InversionError('Result contains non-finite values') return op.source.from_data(R)
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 uses `scipy.linalg.solve_continuous_are`, which is a dense solver. Therefore, we assume all |Operators| and |VectorArrays| can be converted to |NumPy arrays| using :func:`~pymor.algorithms.to_matrix.to_matrix` and :func:`~pymor.vectorarrays.interfaces.VectorArrayInterface.to_numpy`. 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, R, S, trans) options = _parse_options(options, ricc_lrcf_solver_options(), 'scipy', None, False) if options['type'] != 'scipy': raise ValueError(f"Unexpected Riccati equation solver ({options['type']}).") A_source = A.source A = to_matrix(A, format='dense') E = to_matrix(E, format='dense') if E else None B = B.to_numpy().T C = C.to_numpy() S = S.to_numpy().T if S else None if R is None: R = np.eye(C.shape[0] if not trans else B.shape[1]) if not trans: if E is not None: E = E.T X = solve_continuous_are(A.T, C.T, B.dot(B.T), R, E, S) else: X = solve_continuous_are(A, B, C.T.dot(C), R, E, S) return A_source.from_numpy(_chol(X).T)