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 __init__(self, M, E, K, a, b): super().__init__([[IdentityOperator(M.source) * a, IdentityOperator(M.source) * b], [((-b) * K).assemble(), (a * M - b * E).assemble()]]) self.M = M self.E = E self.K = K self.a = a self.b = b
def discretize_cell_problems(diffusion_functions, diffusion_functionals, diameter=1. / 100.): dim = diffusion_functions[0].dim_domain assert dim in (1, 2) assert all(f.dim_domain == dim and f.shape_range == tuple() for f in diffusion_functions) if dim == 1: domain = CircleDomain([0., 1.]) grid, boundary_info = discretize_domain_default(domain, diameter=diameter) visualizer = Matplotlib1DVisualizer(grid=grid, codim=1) else: domain = TorusDomain(([0., 0.], [1., 1.])) grid, boundary_info = discretize_domain_default(domain, diameter=diameter, grid_type=TriaGrid) visualizer = PatchVisualizer(grid=grid, bounding_box=grid.domain, codim=2) operators = [DiffusionOperatorP1(grid, boundary_info, diffusion_function=f, name='diffusion_{}'.format(i)) for i, f in enumerate(diffusion_functions)] operator = LincombOperator(operators, diffusion_functionals) dirichlet_operator = DirichletOperator(operator) mean_value_functional = L2ProductFunctionalP1(grid, ConstantFunction(1., dim_domain=dim), order=1, name='mean_value_functional') constant_projection = Concatenation(VectorOperator(NumpyVectorArray(np.ones(grid.size(dim))), copy=False), mean_value_functional) mean_value_corrector = IdentityOperator(constant_projection.source) - constant_projection mean_value_corrector.unlock() inject_sid(mean_value_corrector, 'cell_problem_mean_value_corrector', grid) ones = NumpyVectorArray(np.ones(grid.size(dim))) def make_diffusion_integral(f): op = ConstantOperator(L2ProductFunctionalP1(grid, f, order=1).apply(ones), source=operator.source) op.unlock() inject_sid(op, 'cell_problem_diffusion_integral', f, grid) return op diffusion_integrals = [make_diffusion_integral(f) for f in diffusion_functions] diffusion_integral = LincombOperator(diffusion_integrals, diffusion_functionals) rhss = [] for dim_ind in range(dim): components = [CellProblemRHSOperator(grid, diffusion_function=f, dim_ind=dim_ind, name='RHS_Functional_{}_{}'.format(dim_ind, i)) for i, f in enumerate(diffusion_functions)] rhss.append(LincombOperator(components, diffusion_functionals)) discretizations = [] for dim_ind in range(dim): rhs = rhss[dim_ind] dirichlet_rhs = DirichletFunctional(rhs) homogenized_diffusions = [(diffusion_integral - rhss[i] if i == dim_ind else rhss[i] * (-1.)) for i in range(dim)] d = ZeroMeanStationaryDiscretization(operator, rhs, dirichlet_operator, dirichlet_rhs, mean_value_corrector, functionals={('diffusion', i): f for i, f in enumerate(homogenized_diffusions)}, visualizer=visualizer, name='CellProblem_{}'.format(dim_ind)) discretizations.append(d) return discretizations, {'grid': grid, 'boundary_info': boundary_info}
def test_block_identity_lincomb(): space = NumpyVectorSpace(10) space2 = BlockVectorSpace([space, space]) identity = BlockDiagonalOperator([IdentityOperator(space), IdentityOperator(space)]) identity2 = IdentityOperator(space2) ones = space.ones() ones2 = space2.make_array([ones, ones]) idid = identity + identity2 assert almost_equal(ones2 * 2, idid.apply(ones2)) assert almost_equal(ones2 * 2, idid.apply_adjoint(ones2)) assert almost_equal(ones2 * 0.5, idid.apply_inverse(ones2)) assert almost_equal(ones2 * 0.5, idid.apply_inverse_adjoint(ones2))
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 __init__(self, fom, RB=None, product=None, coercivity_estimator=None, check_orthonormality=None, check_tol=None): assert isinstance(fom.time_stepper, ImplicitEulerTimeStepper) super().__init__(fom, RB, product=product, check_orthonormality=check_orthonormality, check_tol=check_tol) self.coercivity_estimator = coercivity_estimator self.residual_reductor = ImplicitEulerResidualReductor( self.bases['RB'], fom.operator, fom.mass, fom.T / fom.time_stepper.nt, rhs=fom.rhs, product=product) self.initial_residual_reductor = ResidualReductor( self.bases['RB'], IdentityOperator(fom.solution_space), fom.initial_data, product=fom.l2_product, riesz_representatives=False)
def __init__(self, fom, RB=None, product=None, coercivity_estimator=None, check_orthonormality=None, check_tol=None): if not isinstance(fom.time_stepper, ImplicitEulerTimeStepper): raise NotImplementedError if fom.mass is not None and fom.mass.parametric and 't' in fom.mass.parameters: raise NotImplementedError super().__init__(fom, RB, product=product, check_orthonormality=check_orthonormality, check_tol=check_tol) self.coercivity_estimator = coercivity_estimator self.residual_reductor = ImplicitEulerResidualReductor( self.bases['RB'], fom.operator, fom.mass, fom.T / fom.time_stepper.nt, rhs=fom.rhs, product=product) self.initial_residual_reductor = ResidualReductor( self.bases['RB'], IdentityOperator(fom.solution_space), fom.initial_data, product=fom.l2_product, riesz_representatives=False)
def action_BlockSpaceIdentityOperator(self, ops): new_ops = tuple( BlockDiagonalOperator( [IdentityOperator(s) for s in op.source. subspaces]) if isinstance(op, IdentityOperator) else op for op in ops if not isinstance(op, ZeroOperator)) return self.apply(new_ops)
def __init__(self, T, initial_data, operator, rhs, mass=None, time_stepper=None, num_values=None, output_functional=None, products=None, error_estimator=None, visualizer=None, name=None): if isinstance(rhs, VectorArray): assert rhs in operator.range rhs = VectorOperator(rhs, name='rhs') if isinstance(initial_data, VectorArray): assert initial_data in operator.source initial_data = VectorOperator(initial_data, name='initial_data') mass = mass or IdentityOperator(operator.source) rhs = rhs or ZeroOperator(operator.source, NumpyVectorSpace(1)) assert isinstance(time_stepper, TimeStepper) assert initial_data.source.is_scalar assert operator.source == initial_data.range assert rhs.linear and rhs.range == operator.range and rhs.source.is_scalar assert mass.linear and mass.source == mass.range == operator.source assert output_functional is None or output_functional.source == operator.source super().__init__(products=products, error_estimator=error_estimator, visualizer=visualizer, name=name) self.parameters_internal = {'t': 1} self.__auto_init(locals()) self.solution_space = operator.source self.linear = operator.linear and (output_functional is None or output_functional.linear) if output_functional is not None: self.dim_output = output_functional.range.dim
def _assemble_lincomb_preprocess_operators(self, operators): return [ BlockDiagonalOperator([IdentityOperator(s) for s in op.source.subspaces], source_id=op.source.id, range_id=op.range.id) if isinstance(op, IdentityOperator) else op for op in operators if not isinstance(op, ZeroOperator) ]
def action_IdentityOperator(self, ops): coeff = sum(self.coefficients) if coeff == 0: return ZeroOperator(ops[0].source, ops[0].source, name=self.name) else: return LincombOperator( [IdentityOperator(ops[0].source, name=self.name)], [coeff], name=self.name)
def action_IdentityOperator(self, op): dim_range, dim_source = self.dim_range, self.dim_source if dim_range != dim_source: raise RuleNotMatchingError( 'dim_range and dim_source must be equal.') space = op.source if dim_source is None else NumpyVectorSpace( dim_source) return IdentityOperator(space, name=op.name)
def test_identity_lincomb(): space = NumpyVectorSpace(10) identity = IdentityOperator(space) ones = space.ones() idid = (identity + identity) assert almost_equal(ones * 2, idid.apply(ones)) assert almost_equal(ones * 2, idid.apply_adjoint(ones)) assert almost_equal(ones * 0.5, idid.apply_inverse(ones)) assert almost_equal(ones * 0.5, idid.apply_inverse_adjoint(ones))
def __init__(self, block_space, component): assert isinstance(block_space, BlockVectorSpace) assert 0 <= component < len(block_space.subspaces) blocks = [ ZeroOperator(space, space) if i != component else IdentityOperator(space) for i, space in enumerate(block_space.subspaces) ] super().__init__(blocks)
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_lincomb(): space = NumpyVectorSpace(10) identity = IdentityOperator(space) ones = space.ones() idid = (identity + identity) idid_ = ExpressionParameterFunctional('2', {}) * identity assert almost_equal(ones * 2, idid.apply(ones)) assert almost_equal(ones * 2, idid.apply_adjoint(ones)) assert almost_equal(ones * 0.5, idid.apply_inverse(ones)) assert almost_equal(ones * 0.5, idid.apply_inverse_adjoint(ones)) assert almost_equal(ones * 0.5, idid_.apply_inverse(ones)) assert almost_equal(ones * 0.5, idid_.apply_inverse_adjoint(ones))
def apeinv_apply(self, op, p, idx_p, y): y = self.a.source.from_numpy(y.T) e = IdentityOperator(self.a.source) if self.e is None else self.e if p.imag == 0: ape = self.a + p.real * e else: ape = self.a + p * e if op == pymess.MESS_OP_NONE: x = ape.apply_inverse(y) else: x = ape.apply_inverse_adjoint(y.conj()).conj() return x.to_numpy().T
def __init__(self, d, RB=None, product=None, coercivity_estimator=None): assert isinstance(d.time_stepper, ImplicitEulerTimeStepper) super().__init__(d, RB, product=product) self.coercivity_estimator = coercivity_estimator self.residual_reductor = ImplicitEulerResidualReductor( self.RB, d.operator, d.mass, d.T / d.time_stepper.nt, functional=d.rhs, product=product) self.initial_residual_reductor = ResidualReductor( self.RB, IdentityOperator(d.solution_space), d.initial_data, product=d.l2_product)
def __init__(self, E, K): super().__init__([[None, IdentityOperator(E.source)], [K * (-1), E * (-1)]]) self.__auto_init(locals())
def thermalblock_identity_factory(xblocks, yblocks, diameter, seed): from pymor.operators.constructions import IdentityOperator _, _, U, V, sp, rp = thermalblock_factory(xblocks, yblocks, diameter, seed) return IdentityOperator(U.space), None, U, V, sp, rp
def __init__(self, M, E, K, a, b): super().__init__( [[IdentityOperator(M.source) * a, IdentityOperator(M.source) * b], [((-b) * K).assemble(), (a * M - b * E).assemble()]]) self.__auto_init(locals())
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_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 reduce(self, r, projection='bfsr'): """Reduce using SOBT. Parameters ---------- r Order of the reduced model. projection Projection method used: - `'sr'`: square root method - `'bfsr'`: balancing-free square root method (default, since it avoids scaling by singular values and orthogonalizes the projection matrices, which might make it more accurate than the square root method) - `'biorth'`: like the balancing-free square root method, except it biorthogonalizes the projection matrices Returns ------- rom Reduced-order |SecondOrderModel|. """ assert 0 < r < self.fom.order assert projection in ('sr', 'bfsr', 'biorth') # compute all necessary Gramian factors pcf = self.fom.gramian('pc_lrcf', mu=self.mu) pof = self.fom.gramian('po_lrcf', mu=self.mu) vcf = self.fom.gramian('vc_lrcf', mu=self.mu) vof = self.fom.gramian('vo_lrcf', mu=self.mu) if r > min(len(pcf), len(pof), len(vcf), len(vof)): raise ValueError( 'r needs to be smaller than the sizes of Gramian factors.') # find necessary SVDs Up, sp, Vp = spla.svd(pof.inner(pcf), lapack_driver='gesvd') Up = Up.T Uv, sv, Vv = spla.svd(vof.inner(vcf, product=self.fom.M), lapack_driver='gesvd') Uv = Uv.T # compute projection matrices and find the reduced model self.V1 = pcf.lincomb(Vp[:r]) self.W1 = pof.lincomb(Up[:r]) self.V2 = vcf.lincomb(Vv[:r]) self.W2 = vof.lincomb(Uv[:r]) if projection == 'sr': alpha1 = 1 / np.sqrt(sp[:r]) self.V1.scal(alpha1) self.W1.scal(alpha1) alpha2 = 1 / np.sqrt(sv[:r]) self.V2.scal(alpha2) self.W2.scal(alpha2) W1TV1invW1TV2 = self.W1.inner(self.V2) projected_ops = {'M': IdentityOperator(NumpyVectorSpace(r))} elif projection == 'bfsr': gram_schmidt(self.V1, atol=0, rtol=0, copy=False) gram_schmidt(self.W1, atol=0, rtol=0, copy=False) gram_schmidt(self.V2, atol=0, rtol=0, copy=False) gram_schmidt(self.W2, atol=0, rtol=0, copy=False) W1TV1invW1TV2 = spla.solve(self.W1.inner(self.V1), self.W1.inner(self.V2)) projected_ops = { 'M': project(self.fom.M, range_basis=self.W2, source_basis=self.V2) } elif projection == 'biorth': gram_schmidt_biorth(self.V1, self.W1, copy=False) gram_schmidt_biorth(self.V2, self.W2, product=self.fom.M, copy=False) W1TV1invW1TV2 = self.W1.inner(self.V2) projected_ops = {'M': IdentityOperator(NumpyVectorSpace(r))} projected_ops.update({ 'E': project(self.fom.E.assemble(mu=self.mu), range_basis=self.W2, source_basis=self.V2), 'K': project(self.fom.K.assemble(mu=self.mu), range_basis=self.W2, source_basis=self.V1.lincomb(W1TV1invW1TV2.T)), 'B': project(self.fom.B.assemble(mu=self.mu), range_basis=self.W2, source_basis=None), 'Cp': project(self.fom.Cp.assemble(mu=self.mu), range_basis=None, source_basis=self.V1.lincomb(W1TV1invW1TV2.T)), 'Cv': project(self.fom.Cv.assemble(mu=self.mu), range_basis=None, source_basis=self.V2), 'D': self.fom.D.assemble(mu=self.mu), }) rom = SecondOrderModel(name=self.fom.name + '_reduced', **projected_ops) rom.disable_logging() return rom
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 eigs(A, E=None, k=3, which='LM', b=None, l=None, maxiter=1000, tol=1e-13, imag_tol=1e-12, complex_pair_tol=1e-12, seed=0): """Approximate a few eigenvalues of a linear |Operator|. Computes `k` eigenvalues `w` with corresponding eigenvectors `v` which solve the eigenvalue problem .. math:: A v_i = w_i v_i or the generalized eigenvalue problem .. math:: A v_i = w_i E v_i if `E` is not `None`. The implementation is based on Algorithm 4.2 in :cite:`RL95`. Parameters ---------- A The real linear |Operator| for which the eigenvalues are to be computed. E The real linear |Operator| which defines the generalized eigenvalue problem. k The number of eigenvalues and eigenvectors which are to be computed. which A string specifying which `k` eigenvalues and eigenvectors to compute: - `'LM'`: select eigenvalues with largest magnitude - `'SM'`: select eigenvalues with smallest magnitude - `'LR'`: select eigenvalues with largest real part - `'SR'`: select eigenvalues with smallest real part - `'LI'`: select eigenvalues with largest imaginary part - `'SI'`: select eigenvalues with smallest imaginary part b Initial vector for Arnoldi iteration. Default is a random vector. l The size of the Arnoldi factorization. Default is `min(n - 1, max(2*k + 1, 20))`. maxiter The maximum number of iterations. tol The relative error tolerance for the Ritz estimates. imag_tol Relative imaginary parts below this tolerance are set to 0. complex_pair_tol Tolerance for detecting pairs of complex conjugate eigenvalues. seed Random seed which is used for computing the initial vector for the Arnoldi iteration. Returns ------- w A 1D |NumPy array| which contains the computed eigenvalues. v A |VectorArray| which contains the computed eigenvectors. """ logger = getLogger('pymor.algorithms.eigs.eigs') assert isinstance(A, Operator) and A.linear assert not A.parametric assert A.source == A.range if E is None: E = IdentityOperator(A.source) else: assert isinstance(E, Operator) and E.linear assert not E.parametric assert E.source == E.range assert E.source == A.source if b is None: b = A.source.random(seed=seed) else: assert b in A.source n = A.source.dim l_min = 20 if l is None: l = min(n - 1, max(2 * k + 1, l_min)) assert k < n assert l > k V, H, f = _arnoldi(A, E, k, b) k0 = k i = 0 while True: i += 1 V, H, f = _extend_arnoldi(A, E, V, H, f, l - k) ew, ev = spla.eig(H) # truncate small imaginary parts ew.imag[np.abs(ew.imag) / np.abs(ew) < imag_tol] = 0 if which == 'LM': idx = np.argsort(-np.abs(ew)) elif which == 'SM': idx = np.argsort(np.abs(ew)) elif which == 'LR': idx = np.argsort(-ew.real) elif which == 'SR': idx = np.argsort(ew.real) elif which == 'LI': idx = np.argsort(-np.abs(ew.imag)) elif which == 'SI': idx = np.argsort(np.abs(ew.imag)) k = k0 ews = ew[idx] evs = ev[:, idx] rres = f.norm()[0] * np.abs(evs[l - 1]) / np.abs(ews) # increase k by one in order to keep complex conjugate pairs together if ews[k - 1].imag != 0 and ews[ k - 1].imag + ews[k].imag < complex_pair_tol: k += 1 logger.info( f'Maximum of relative Ritz estimates at step {i}: {rres[:k].max():.5e}' ) if np.all(rres[:k] <= tol) or i >= maxiter: break # increase k in order to prevent stagnation k = min(l - 1, k + min(np.count_nonzero(rres[:k] <= tol), (l - k) // 2)) # sort shifts for QR iteration based on their residual shifts = ews[k:l] srres = rres[k:l] idx = np.argsort(-srres) srres = srres[idx] shifts = shifts[idx] # don't use converged unwanted Ritz values as shifts shifts = shifts[srres != 0] k += np.count_nonzero(srres == 0) if shifts[0].imag != 0 and shifts[0].imag + ews[ 1].imag >= complex_pair_tol: shifts = shifts[1:] k += 1 H, Qs = _qr_iteration(H, shifts) V = V.lincomb(Qs.T) f = V[k] * H[k, k - 1] + f * Qs[l - 1, k - 1] V = V[:k] H = H[:k, :k] return ews[:k0], V.lincomb(evs[:, :k0].T)
def __init__(self, E, K): super().__init__([[None, IdentityOperator(E.source)], [K * (-1), E * (-1)]]) self.E = E self.K = K
def reduce_parabolic(discretization, RB, product=None, coercivity_estimator=None, disable_caching=True, extends=None): r"""Reductor for parabolic equations. This reductor uses :meth:`~pymor.reductors.basic.reduce_generic_rb` for the actual RB-projection. The only addition is the assembly of an error estimator which bounds the discrete l2-in time / energy-in space error similar to [GP05]_, [HO08]_ as follows: .. math:: \left[ C_a^{-1}(\mu)\|e_N(\mu)\|^2 + \sum_{n=1}^{N} dt\|e_n(\mu)\|^2_e \right]^{1/2} \leq \left[ C_a^{-1}(\mu)dt \sum_{n=1}^{N}\|\mathcal{R}^n(u_n(\mu), \mu)\|^2_{e,-1} + C_a^{-1}(\mu)\|e_0\|^2 \right]^{1/2} Here, :math:`\|\cdot\|` denotes the norm induced by the problem's mass matrix (e.g. the L^2-norm) and :math:`\|\cdot\|_e` is an arbitrary energy norm w.r.t. which the space operator :math:`A(\mu)` is coercive, and :math:`C_a(\mu)` is a lower bound for its coercivity constant. Finally, :math:`\mathcal{R}^n` denotes the implicit Euler timestepping residual for the (fixed) time step size :math:`dt`, .. math:: \mathcal{R}^n(u_n(\mu), \mu) := f - M \frac{u_{n}(\mu) - u_{n-1}(\mu)}{dt} - A(u_n(\mu), \mu), where :math:`M` denotes the mass operator and :math:`f` the source term. The dual residual norm is evaluated using the numerically stable projection from [BEOR14]_. .. warning:: The reduced basis `RB` is required to be orthonormal w.r.t. the given energy product. If not, the projection of the initial values will be computed incorrectly. .. [GP05] M. A. Grepl, A. T. Patera, A Posteriori Error Bounds For Reduced-Basis Approximations Of Parametrized Parabolic Partial Differential Equations, M2AN 39(1), 157-181, 2005. .. [HO08] B. Haasdonk, M. Ohlberger, Reduced basis method for finite volume approximations of parametrized evolution equations, M2AN 42(2), 277-302, 2008. .. [BEOR14] A. Buhr, C. Engwer, M. Ohlberger, S. Rave, A Numerically Stable A Posteriori Error Estimator for Reduced Basis Approximations of Elliptic Equations, Proceedings of the 11th World Congress on Computational Mechanics, 2014. Parameters ---------- discretization The |InstationaryDiscretization| which is to be reduced. RB |VectorArray| containing the reduced basis on which to project. product The energy inner product |Operator| w.r.t. the reduction error is estimated. RB must be to be orthonomrmal w.r.t. this product! coercivity_estimator `None` or a |Parameterfunctional| returning a lower bound for the coercivity constant of `discretization.operator` w.r.t. `product`. disable_caching If `True`, caching of solutions is disabled for the reduced |Discretization|. extends See :meth:`~pymor.algorithms.greedy.greedy`. Returns ------- rd The reduced |Discretization|. rc The reconstructor providing a `reconstruct(U)` method which reconstructs high-dimensional solutions from solutions `U` of the reduced |Discretization|. reduction_data Additional data produced by the reduction process. (See :meth:`~pymor.algorithms.greedy.greedy`.) """ assert extends is None or len(extends) == 3 assert isinstance(discretization.time_stepper, ImplicitEulerTimeStepper) logger = getLogger('pymor.reductors.parabolic.reduce_parabolic') old_residual_data = extends[2].pop('residual') if extends else None old_initial_resdidual_data = extends[2].pop( 'initial_residual') if extends else None with logger.block('RB projection ...'): rd, rc, data = reduce_generic_rb(discretization, RB, vector_product=product, disable_caching=disable_caching, extends=extends) dt = discretization.T / discretization.time_stepper.nt with logger.block('Assembling error estimator ...'): residual, residual_reconstructor, residual_data = reduce_implicit_euler_residual( discretization.operator, discretization.mass, dt, discretization.rhs, RB, product=product, extends=old_residual_data) initial_residual, initial_residual_reconstructor, initial_residual_data = reduce_residual( IdentityOperator(discretization.solution_space), discretization.initial_data, RB, False, product=discretization.l2_product, extends=old_initial_resdidual_data) estimator = ReduceParabolicEstimator( residual, residual_data.get('residual_range_dims', None), initial_residual, initial_residual_data.get('residual_range_dims', None), coercivity_estimator) rd = rd.with_(estimator=estimator) data.update(residual=(residual, residual_reconstructor, residual_data), initial_residual=(initial_residual, initial_residual_reconstructor, initial_residual_data)) return rd, rc, data
def test_to_matrix_IdentityOperator(): n = 3 I = np.eye(n) Iop = IdentityOperator(NumpyVectorSpace(n)) assert_type_and_allclose(I, Iop, 'sparse')
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 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 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