def reduce(self, r=None, tol=None, projection='bfsr'): """Generic Balanced Truncation. Parameters ---------- r Order of the reduced model if `tol` is `None`, maximum order if `tol` is specified. tol Tolerance for the error bound if `r` is `None`. 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 (using :func:`~pymor.algorithms.gram_schmidt.gram_schmidt_biorth`) Returns ------- rom Reduced-order model. """ assert r is not None or tol is not None assert r is None or 0 < r < self.fom.order assert projection in ('sr', 'bfsr', 'biorth') cf, of = self._gramians() sv, sU, sV = self._sv_U_V() # find reduced order if tol is specified if tol is not None: error_bounds = self.error_bounds() r_tol = np.argmax(error_bounds <= tol) + 1 r = r_tol if r is None else min(r, r_tol) if r > min(len(cf), len(of)): raise ValueError( 'r needs to be smaller than the sizes of Gramian factors.') # compute projection matrices self.V = cf.lincomb(sV[:r]) self.W = of.lincomb(sU[:r]) if projection == 'sr': alpha = 1 / np.sqrt(sv[:r]) self.V.scal(alpha) self.W.scal(alpha) elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=self.fom.E) # find reduced-order model self._pg_reductor = LTIPGReductor(self.fom, self.W, self.V, projection in ('sr', 'biorth')) rom = self._pg_reductor.reduce() return rom
def reduce(self, r, projection='bfsr'): """Reduce using SOBTfv. 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) if r > min(len(pcf), len(pof)): raise ValueError( 'r needs to be smaller than the sizes of Gramian factors.') # find necessary SVDs _, sp, Vp = spla.svd(pof.inner(pcf), lapack_driver='gesvd') # compute projection matrices self.V = pcf.lincomb(Vp[:r]) if projection == 'sr': alpha = 1 / np.sqrt(sp[:r]) self.V.scal(alpha) elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) elif projection == 'biorth': self.V = gram_schmidt(self.V, product=self.fom.M, atol=0, rtol=0) self.W = self.V # find the reduced model if self.fom.parametric: fom_mu = self.fom.with_(**{ op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['M', 'E', 'K', 'B', 'Cp', 'Cv'] }, parameter_space=None) else: fom_mu = self.fom self._pg_reductor = SOLTIPGReductor(fom_mu, self.W, self.V, projection == 'biorth') rom = self._pg_reductor.reduce() return rom
def reduce(self, r, projection='bfsr'): """Reduce using GenericSOBTpv. 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 gramians = self._gramians() if r > min(len(g) for g in gramians): raise ValueError( 'r needs to be smaller than the sizes of Gramian factors.') # compute projection matrices self.V, self.W, singular_values = self._projection_matrices_and_singular_values( r, gramians) if projection == 'sr': alpha = 1 / np.sqrt(singular_values[:r]) self.V.scal(alpha) self.W.scal(alpha) elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=self.fom.M) # find the reduced model if self.fom.parametric: fom_mu = self.fom.with_(**{ op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['M', 'E', 'K', 'B', 'Cp', 'Cv'] }, parameter_space=None) else: fom_mu = self.fom self._pg_reductor = SOLTIPGReductor(fom_mu, self.W, self.V, projection == 'biorth') rom = self._pg_reductor.reduce() return rom
def _projection_matrices(self, rom, projection): if self.fom.parametric: fom = self.fom.with_(**{ op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['A', 'B', 'C', 'D', 'E'] }, parameter_space=None) else: fom = self.fom self.V, self.W = solve_sylv_schur(fom.A, rom.A, E=fom.E, Er=rom.E, B=fom.B, Br=rom.B, C=fom.C, Cr=rom.C) if projection == 'orth': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=fom.E) self._pg_reductor = LTIPGReductor(fom, self.W, self.V, projection == 'biorth')
def extend_basis(U, basis, product=None, method='gram_schmidt', pod_modes=1, pod_orthonormalize=True, copy_U=True): assert method in ('trivial', 'gram_schmidt', 'pod') basis_length = len(basis) if method == 'trivial': remove = set() for i in range(len(U)): if np.any(almost_equal(U[i], basis)): remove.add(i) basis.append(U[[i for i in range(len(U)) if i not in remove]], remove_from_other=(not copy_U)) elif method == 'gram_schmidt': basis.append(U, remove_from_other=(not copy_U)) gram_schmidt(basis, offset=basis_length, product=product, copy=False, check=False) elif method == 'pod': U_proj_err = U - basis.lincomb(U.inner(basis, product)) basis.append(pod(U_proj_err, modes=pod_modes, product=product, orthonormalize=False)[0]) if pod_orthonormalize: gram_schmidt(basis, offset=basis_length, product=product, copy=False, check=False) if len(basis) <= basis_length: raise ExtensionError
def _set_V_reductor(self, sigma, b, c, projection): fom = ( self.fom.with_( **{op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['A', 'B', 'C', 'D', 'E']}, parameter_space=None, ) if self.fom.parametric else self.fom ) if self.version == 'V': self.V = tangential_rational_krylov(fom.A, fom.E, fom.B, b, sigma, orth=False) gram_schmidt(self.V, atol=0, rtol=0, product=None if projection == 'orth' else fom.E, copy=False) else: self.V = tangential_rational_krylov(fom.A, fom.E, fom.C, c, sigma, trans=True, orth=False) gram_schmidt(self.V, atol=0, rtol=0, product=None if projection == 'orth' else fom.E, copy=False) self.W = self.V self._pg_reductor = LTIPGReductor(fom, self.V, self.V, projection == 'Eorth')
def _projection_matrix(self, r, sigma, b, c, projection): fom = self.fom if self.version == 'V': V = fom.A.source.empty(reserve=r) else: W = fom.A.source.empty(reserve=r) for i in range(r): if sigma[i].imag == 0: sEmA = sigma[i].real * self.fom.E - self.fom.A if self.version == 'V': Bb = fom.B.apply(b.real[i]) V.append(sEmA.apply_inverse(Bb)) else: CTc = fom.C.apply_adjoint(c.real[i]) W.append(sEmA.apply_inverse_adjoint(CTc)) elif sigma[i].imag > 0: sEmA = sigma[i] * self.fom.E - self.fom.A if self.version == 'V': Bb = fom.B.apply(b[i]) v = sEmA.apply_inverse(Bb) V.append(v.real) V.append(v.imag) else: CTc = fom.C.apply_adjoint(c[i].conj()) w = sEmA.apply_inverse_adjoint(CTc) W.append(w.real) W.append(w.imag) if self.version == 'V': self.V = gram_schmidt(V, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) else: self.V = gram_schmidt(W, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) self._pg_reductor = LTIPGReductor(fom, self.V, self.V, projection == 'Eorth')
def extend_basis(U, basis, product=None, method='gram_schmidt', pod_modes=1, pod_orthonormalize=True, copy_U=True): assert method in ('trivial', 'gram_schmidt', 'pod') basis_length = len(basis) if method == 'trivial': remove = set() for i in range(len(U)): if np.any(almost_equal(U[i], basis)): remove.add(i) basis.append(U[[i for i in range(len(U)) if i not in remove]], remove_from_other=(not copy_U)) elif method == 'gram_schmidt': basis.append(U, remove_from_other=(not copy_U)) gram_schmidt(basis, offset=basis_length, product=product, copy=False, check=False) elif method == 'pod': U_proj_err = U - basis.lincomb(U.inner(basis, product)) basis.append(pod(U_proj_err, modes=pod_modes, product=product, orth_tol=np.inf)[0]) if pod_orthonormalize: gram_schmidt(basis, offset=basis_length, product=product, copy=False, check=False) if len(basis) <= basis_length: raise ExtensionError
def test_gram_schmidt(): for i in (1, 32): b = NumpyVectorArray(np.identity(i, dtype=np.float)) a = gram_schmidt(b) assert np.all(almost_equal(b, a)) c = NumpyVectorArray([[1.0, 0], [0., 0]]) a = gram_schmidt(c) assert (a.data == np.array([[1.0, 0]])).all()
def reduce(self, r, projection='bfsr'): """Reduce using SOBTfv. 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 system. """ assert 0 < r < self.fom.order assert projection in ('sr', 'bfsr', 'biorth') # compute all necessary Gramian factors pcf = self.fom.gramian('pc_lrcf') pof = self.fom.gramian('po_lrcf') if r > min(len(pcf), len(pof)): raise ValueError( 'r needs to be smaller than the sizes of Gramian factors.') # find necessary SVDs _, sp, Vp = spla.svd(pof.inner(pcf)) # compute projection matrices and find the reduced model self.V = pcf.lincomb(Vp[:r]) if projection == 'sr': alpha = 1 / np.sqrt(sp[:r]) self.V.scal(alpha) self.bases_are_biorthonormal = False elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.bases_are_biorthonormal = False elif projection == 'biorth': self.V = gram_schmidt(self.V, product=self.fom.M, atol=0, rtol=0) self.bases_are_biorthonormal = True self.W = self.V self.pg_reductor = SOLTIPGReductor(self.fom, self.W, self.V, projection == 'biorth') rom = self.pg_reductor.reduce() return rom
def reduce(self, r, projection='bfsr'): """Reduce using SOBTfv. 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 ------- rd Reduced system. """ assert 0 < r < self.d.n assert projection in ('sr', 'bfsr', 'biorth') # compute all necessary Gramian factors pcf = self.d.gramian('pcf') pof = self.d.gramian('pof') if r > min(len(pcf), len(pof)): raise ValueError('r needs to be smaller than the sizes of Gramian factors.') # find necessary SVDs _, sp, Vp = spla.svd(pof.inner(pcf)) # compute projection matrices and find the reduced model self.V = pcf.lincomb(Vp[:r]) if projection == 'sr': alpha = 1 / np.sqrt(sp[:r]) self.V.scal(alpha) self.bases_are_biorthonormal = False elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.bases_are_biorthonormal = False elif projection == 'biorth': self.V = gram_schmidt(self.V, product=self.d.M, atol=0, rtol=0) self.bases_are_biorthonormal = True self.W = self.V self.pg_reductor = GenericPGReductor(self.d, self.W, self.V, projection == 'biorth', product=self.d.M) rd = self.pg_reductor.reduce() return rd
def reduce(self, r=None, tol=None, projection='bfsr'): """Generic Balanced Truncation. Parameters ---------- r Order of the reduced model if `tol` is `None`, maximum order if `tol` is specified. tol Tolerance for the error bound if `r` is `None`. 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 (using :func:`~pymor.algorithms.gram_schmidt.gram_schmidt_biorth`) Returns ------- rom Reduced-order model. """ assert r is not None or tol is not None assert r is None or 0 < r < self.fom.order assert projection in ('sr', 'bfsr', 'biorth') cf, of = self._gramians() sv, sU, sV = self._sv_U_V() # find reduced order if tol is specified if tol is not None: error_bounds = self.error_bounds() r_tol = np.argmax(error_bounds <= tol) + 1 r = r_tol if r is None else min(r, r_tol) if r > min(len(cf), len(of)): raise ValueError('r needs to be smaller than the sizes of Gramian factors.') # compute projection matrices self.V = cf.lincomb(sV[:r]) self.W = of.lincomb(sU[:r]) if projection == 'sr': alpha = 1 / np.sqrt(sv[:r]) self.V.scal(alpha) self.W.scal(alpha) elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=self.fom.E) # find reduced-order model self._pg_reductor = LTIPGReductor(self.fom, self.W, self.V, projection in ('sr', 'biorth')) rom = self._pg_reductor.reduce() return rom
def test_gram_schmidt(vector_array): U = vector_array V = U.copy() onb = gram_schmidt(U, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(onb.dot(onb), np.eye(len(onb))) assert np.all(almost_equal(U, onb.lincomb(U.dot(onb)), rtol=1e-13)) onb2 = gram_schmidt(U, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(almost_equal(onb, U))
def test_gram_schmidt_with_product(operator_with_arrays_and_products): _, _, U, _, p, _ = operator_with_arrays_and_products V = U.copy() onb = gram_schmidt(U, product=p, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(p.apply2(onb, onb), np.eye(len(onb))) assert np.all(almost_equal(U, onb.lincomb(p.apply2(U, onb)), rtol=1e-13)) onb2 = gram_schmidt(U, product=p, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(almost_equal(onb, U))
def test_gram_schmidt_with_product(operator_with_arrays_and_products): _, _, U, _, p, _ = operator_with_arrays_and_products V = U.copy() onb = gram_schmidt(U, product=p, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(p.apply2(onb, onb), np.eye(len(onb))) assert np.all(almost_equal(U, onb.lincomb(p.apply2(onb, U).T), rtol=1e-13)) onb2 = gram_schmidt(U, product=p, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(almost_equal(onb, U))
def _projection_matrices(self, rom, projection): fom = self.fom self.V, self.W = solve_sylv_schur(fom.A, rom.A, E=fom.E, Er=rom.E, B=fom.B, Br=rom.B, C=fom.C, Cr=rom.C) if projection == 'orth': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=fom.E) self._pg_reductor = LTIPGReductor(fom, self.W, self.V, projection == 'biorth')
def test_gram_schmidt_with_R(vector_array): U = vector_array V = U.copy() onb, R = gram_schmidt(U, return_R=True, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(onb.dot(onb), np.eye(len(onb))) assert np.all(almost_equal(U, onb.lincomb(U.dot(onb)), rtol=1e-13)) assert np.all(almost_equal(V, onb.lincomb(R.T))) onb2, R2 = gram_schmidt(U, return_R=True, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(R == R2) assert np.all(almost_equal(onb, U))
def reduce(self, r, projection='bfsr'): """Reduce using SOBTp. 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 ------- rd Reduced system. """ assert 0 < r < self.d.n assert projection in ('sr', 'bfsr', 'biorth') # compute all necessary Gramian factors gramians = self.gramians() if r > min(len(g) for g in gramians): raise ValueError('r needs to be smaller than the sizes of Gramian factors.') # compute projection matrices and find the reduced model self.V, self.W, singular_values = self.projection_matrices_and_singular_values(r, gramians) if projection == 'sr': alpha = 1 / np.sqrt(singular_values[:r]) self.V.scal(alpha) self.W.scal(alpha) elif projection == 'bfsr': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=self.d.M) self.pg_reductor = GenericPGReductor(self.d, self.W, self.V, projection == 'biorth', product=self.d.M) rd = self.pg_reductor.reduce() return rd
def _projection_matrix(self, r, sigma, b, c, projection): if self.fom.parametric: fom = self.fom.with_(**{ op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['A', 'B', 'C', 'D', 'E'] }, parameter_space=None) else: fom = self.fom if self.version == 'V': V = fom.A.source.empty(reserve=r) else: W = fom.A.source.empty(reserve=r) for i in range(r): if sigma[i].imag == 0: sEmA = sigma[i].real * fom.E - fom.A if self.version == 'V': Bb = fom.B.apply(b.real[i]) V.append(sEmA.apply_inverse(Bb)) else: CTc = fom.C.apply_adjoint(c.real[i]) W.append(sEmA.apply_inverse_adjoint(CTc)) elif sigma[i].imag > 0: sEmA = sigma[i] * fom.E - fom.A if self.version == 'V': Bb = fom.B.apply(b[i]) v = sEmA.apply_inverse(Bb) V.append(v.real) V.append(v.imag) else: CTc = fom.C.apply_adjoint(c[i].conj()) w = sEmA.apply_inverse_adjoint(CTc) W.append(w.real) W.append(w.imag) if self.version == 'V': self.V = gram_schmidt( V, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) else: self.V = gram_schmidt( W, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) self._pg_reductor = LTIPGReductor(fom, self.V, self.V, projection == 'Eorth')
def projection_shifts_init(A, E, B, shift_options): """Find starting shift parameters for low-rank ADI iteration using Galerkin projection on spaces spanned by LR-ADI iterates. See :cite:`PK16`, pp. 92-95. Parameters ---------- A The |Operator| A from the corresponding Lyapunov equation. E The |Operator| E from the corresponding Lyapunov equation. B The |VectorArray| B from the corresponding Lyapunov equation. shift_options The shift options to use (see :func:`lyap_lrcf_solver_options`). Returns ------- shifts A |NumPy array| containing a set of stable shift parameters. """ random_state = get_random_state(seed=shift_options['init_seed']) for i in range(shift_options['init_maxiter']): Q = gram_schmidt(B, atol=0, rtol=0) shifts = spla.eigvals(A.apply2(Q, Q), E.apply2(Q, Q)) shifts = shifts[shifts.real < 0] if shifts.size == 0: # use random subspace instead of span{B} (with same dimensions) B = B.random(len(B), distribution='normal', random_state=random_state) else: return shifts raise RuntimeError('Could not generate initial shifts for low-rank ADI iteration.')
def _extend_arnoldi(A, E, V, H, f, p): """Extend an existing Arnoldi factorization.""" k = len(V) res = f.norm()[0] H = np.pad(H, ((0, p), (0, p))) H[k, k - 1] = res v = f * (1 / res) V = V.copy() V.append(v) for i in range(k, k + p): v = E.apply_inverse(A.apply(v)) V.append(v) _, R = gram_schmidt(V, return_R=True, atol=0, rtol=0, offset=len(V) - 1, copy=False) H[:i + 2, i] = R[:k + p, i + 1] v = V[-1] return V[:k + p], H, v * R[k + p, k + p]
def extend_bases(self, mu, printing=True, U=None, P=None): if self.unique_basis: U, P = self.extend_unique_basis(mu, U, P) return U, P if U is None: U = self.fom.solve(mu) if P is None: P = self.fom.solve_dual(mu, U=U) try: self.primal_reductor.extend_basis(U) # self.non_assembled_primal_reductor.extend_basis(U) except: pass self.primal_rom = self.primal_reductor.reduce() if self.non_assembled_primal_rom is not None: self.non_assembled_primal_rom = self.non_assembled_primal_reductor.reduce( ) self.bases['RB'] = self.primal_reductor.bases['RB'] self.RBPrimal = self.bases['RB'] self.RBDual.append(P) self.RBDual = gram_schmidt(self.RBDual, product=self.opt_product) an, bn = len(self.RBPrimal), len(self.RBDual) self.dual_intermediate_fom, self.dual_rom, self.dual_reductor = self._build_dual_models( ) self.dual = self.dual_reductor self.bases['DU'] = self.dual_reductor.bases['RB'] if printing: print( 'Enrichment completed... length of Bases are {} and {}'.format( an, bn)) return U, P
def projection_shifts_init(A, E, B, shift_options): """Find starting shift parameters for low-rank ADI iteration using Galerkin projection on spaces spanned by LR-ADI iterates. See [PK16]_, pp. 92-95. Parameters ---------- A The |Operator| A from the corresponding Lyapunov equation. E The |Operator| E from the corresponding Lyapunov equation. B The |VectorArray| B from the corresponding Lyapunov equation. shift_options The shift options to use (see :func:`lyap_solver_options`). Returns ------- shifts A |NumPy array| containing a set of stable shift parameters. """ for i in range(shift_options['init_maxiter']): Q = gram_schmidt(B, atol=0, rtol=0) shifts = spla.eigvals(A.apply2(Q, Q), E.apply2(Q, Q)) shifts = shifts[np.real(shifts) < 0] if shifts.size == 0: # use random subspace instead of span{B} (with same dimensions) if shift_options['init_seed'] is not None: np.random.seed(shift_options['init_seed']) np.random.seed(np.random.random() + i) B = B.space.make_array(np.random.rand(len(B), B.space.dim)) else: return shifts raise RuntimeError('Could not generate initial shifts for low-rank ADI iteration.')
def test_gram_schmidt(vector_array): U = vector_array # TODO assumption here masks a potential issue with the algorithm # where it fails in del instead of a proper error assume(len(U) > 1 or not contains_zero_vector(U)) V = U.copy() onb = gram_schmidt(U, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(onb.dot(onb), np.eye(len(onb))) # TODO maybe raise tolerances again assert np.all( almost_equal(U, onb.lincomb(onb.dot(U).T), atol=1e-13, rtol=1e-13)) onb2 = gram_schmidt(U, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(almost_equal(onb, U))
def test_project_array(): np.random.seed(123) U = NumpyVectorSpace.from_numpy(np.random.random((2, 10))) basis = NumpyVectorSpace.from_numpy(np.random.random((3, 10))) U_p = project_array(U, basis, orthonormal=False) onb = gram_schmidt(basis) U_p2 = project_array(U, onb, orthonormal=True) assert np.all(relative_error(U_p, U_p2) < 1e-10)
def test_project_array(arrays): U, basis = arrays U_p = project_array(U, basis, orthonormal=False) onb = gram_schmidt(basis) U_p2 = project_array(U, onb, orthonormal=True) err = relative_error(U_p, U_p2) tol = np.finfo(np.float64).eps * np.linalg.cond(basis.gramian()) * 100. assert np.all(err < tol)
def gram_schmidt_basis_extension(basis, U, product=None, copy_basis=True, copy_U=True): """Extend basis using Gram-Schmidt orthonormalization. Parameters ---------- basis |VectorArray| containing the basis to extend. U |VectorArray| containing the new basis vectors. product The scalar product w.r.t. which to orthonormalize; if `None`, the Euclidean product is used. copy_basis If `copy_basis` is `False`, the old basis is extended in-place. copy_U If `copy_U` is `False`, the new basis vectors are removed from `U`. Returns ------- new_basis The extended basis. extension_data Dict containing the following fields: :hierarchic: `True` if `new_basis` contains `basis` as its first vectors. Raises ------ ExtensionError Gram-Schmidt orthonormalization fails. This is the case when no vector in `U` is linearly independent from the basis. """ if basis is None: basis = U.empty(reserve=len(U)) basis_length = len(basis) new_basis = basis.copy() if copy_basis else basis new_basis.append(U, remove_from_other=(not copy_U)) gram_schmidt(new_basis, offset=basis_length, product=product, copy=False) if len(new_basis) <= basis_length: raise ExtensionError return new_basis, {'hierarchic': True}
def test_project_array(bases): U = bases[0][:-2] basis = bases[1] U_p = project_array(U, basis, orthonormal=False) onb = gram_schmidt(basis) U_p2 = project_array(U, onb, orthonormal=True) err = relative_error(U_p, U_p2) tol = 3e-10 assert np.all(err < tol)
def test_project_array_with_product(): np.random.seed(123) U = NumpyVectorSpace.from_numpy(np.random.random((1, 10))) basis = NumpyVectorSpace.from_numpy(np.random.random((3, 10))) product = np.random.random((10, 10)) product = NumpyMatrixOperator(product.T.dot(product)) U_p = project_array(U, basis, product=product, orthonormal=False) onb = gram_schmidt(basis, product=product) U_p2 = project_array(U, onb, product=product, orthonormal=True) assert np.all(relative_error(U_p, U_p2, product) < 1e-10)
def test_gram_schmidt_with_R(vector_array): U = vector_array # TODO assumption here masks a potential issue with the algorithm # where it fails in del instead of a proper error assume(len(U) > 1 or not contains_zero_vector(U)) V = U.copy() onb, R = gram_schmidt(U, return_R=True, copy=True) assert np.all(almost_equal(U, V)) assert np.allclose(onb.dot(onb), np.eye(len(onb))) lc = onb.lincomb(onb.dot(U).T) rtol = atol = 1e-13 assert np.all(almost_equal(U, lc, rtol=rtol, atol=atol)) assert np.all(almost_equal(V, onb.lincomb(R.T), rtol=rtol, atol=atol)) onb2, R2 = gram_schmidt(U, return_R=True, copy=False) assert np.all(almost_equal(onb, onb2)) assert np.all(R == R2) assert np.all(almost_equal(onb, U))
def _projection_matrix(self, r, sigma, b, c, projection): fom = self.fom if self.version == 'V': V = fom.A.source.empty(reserve=r) else: W = fom.A.source.empty(reserve=r) for i in range(r): if sigma[i].imag == 0: sEmA = sigma[i].real * self.fom.E - self.fom.A if self.version == 'V': Bb = fom.B.apply(b.real[i]) V.append(sEmA.apply_inverse(Bb)) else: CTc = fom.C.apply_adjoint(c.real[i]) W.append(sEmA.apply_inverse_adjoint(CTc)) elif sigma[i].imag > 0: sEmA = sigma[i] * self.fom.E - self.fom.A if self.version == 'V': Bb = fom.B.apply(b[i]) v = sEmA.apply_inverse(Bb) V.append(v.real) V.append(v.imag) else: CTc = fom.C.apply_adjoint(c[i].conj()) w = sEmA.apply_inverse_adjoint(CTc) W.append(w.real) W.append(w.imag) if self.version == 'V': self.V = gram_schmidt( V, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) else: self.V = gram_schmidt( W, atol=0, rtol=0, product=None if projection == 'orth' else fom.E) self._pg_reductor = LTIPGReductor(fom, self.V, self.V, projection == 'Eorth')
def projection_shifts(A, E, V, prev_shifts): """Find further shift parameters for low-rank ADI iteration using Galerkin projection on spaces spanned by LR-ADI iterates. See [PK16]_, pp. 92-95. Parameters ---------- A The |Operator| A from the corresponding Lyapunov equation. E The |Operator| E from the corresponding Lyapunov equation. V A |VectorArray| representing the currently computed iterate. prev_shifts A |NumPy array| containing the set of all previously used shift parameters. Returns ------- shifts A |NumPy array| containing a set of stable shift parameters. """ if prev_shifts[-1].imag != 0: Q = gram_schmidt(cat_arrays([V.real, V.imag]), atol=0, rtol=0) else: Q = gram_schmidt(V, atol=0, rtol=0) Ap = A.apply2(Q, Q) Ep = E.apply2(Q, Q) shifts = spla.eigvals(Ap, Ep) shifts.imag[abs(shifts.imag) < np.finfo(float).eps] = 0 shifts = shifts[np.real(shifts) < 0] if shifts.size == 0: return prev_shifts else: if np.any(shifts.imag != 0): shifts = shifts[np.abs(shifts).argsort()] else: shifts.sort() return shifts
def _set_V_W_reductor(self, rom, projection): fom = ( self.fom.with_( **{op: getattr(self.fom, op).assemble(mu=self.mu) for op in ['A', 'B', 'C', 'D', 'E']} ) if self.fom.parametric else self.fom ) self.V, self.W = solve_sylv_schur(fom.A, rom.A, E=fom.E, Er=rom.E, B=fom.B, Br=rom.B, C=fom.C, Cr=rom.C) if projection == 'orth': gram_schmidt(self.V, atol=0, rtol=0, copy=False) gram_schmidt(self.W, atol=0, rtol=0, copy=False) elif projection == 'biorth': gram_schmidt_biorth(self.V, self.W, product=fom.E, copy=False) self._pg_reductor = LTIPGReductor(fom, self.W, self.V, projection == 'biorth')
def create_bases3(gq, lq, basis_size, q, transfer='robin', silent=True): # nicht-adaptive Basiserstellung mit power-iteration if not silent: print("creating bases") bases = {} for space in gq["spaces"]: ldict = lq[space] # Basis mit Shift-Loesung initialisieren: if transfer == 'dirichlet': lsol = ldict["local_solution_dirichlet"] else: lsol = ldict["local_solution_robin"] product = ldict["range_product"] if transfer == 'dirichlet': transop = NumpyMatrixOperator(ldict["transfer_matrix_dirichlet"]) else: transop = NumpyMatrixOperator(ldict["transfer_matrix_robin"]) basis = rrf(transop, ldict["source_product"], product, q, basis_size, True) basis.append(lsol) gram_schmidt(basis, product, copy=False) bases[space] = basis return bases
def create_bases2(gq, lq, basis_size, transfer='robin', silent=True): # nicht-adaptive Basiserstellung (Algorithmus 4) if not silent: print("creating bases") bases = {} for space in gq["spaces"]: ldict = lq[space] # Basis mit Shift-Loesung initialisieren: if transfer == 'dirichlet': lsol = ldict["local_solution_dirichlet"] else: lsol = ldict["local_solution_robin"] product = ldict["range_product"] if transfer == 'dirichlet': transop = ldict["dirichlet_transfer"] else: transop = ldict["robin_transfer"] basis = rrf(transop, ldict["source_product"], product, 0, basis_size, True) basis.append(lsol) gram_schmidt(basis, product, copy=False) bases[space] = basis return bases
def create_bases(gq, lq, num_testvecs, transfer='robin', testlimit=None, target_accuracy=1e-3, max_failure_probability=1e-15, silent=True, calC=True): # adaptive Basiserstellung (Algorithmus 3) # Berechnung der Konstanten: if calC: if not silent: print("calculating constants") # calculate_lambda_min(gq, lq) calculate_Psi_norm(gq, lq) calculate_continuity_constant(gq, lq) calculate_inf_sup_constant2(gq, lq) calculate_csis(gq, lq) if not silent: print("creating bases") # Basisgenerierung: bases = {} for space in gq["spaces"]: ldict = lq[space] # Basis mit Shift-Loesung initialisieren: if transfer == 'dirichlet': lsol = ldict["local_solution_dirichlet"] else: lsol = ldict["local_solution_robin"] product = ldict["range_product"] if transfer == 'dirichlet': transop = ldict["dirichlet_transfer"] else: transop = ldict["robin_transfer"] tol_i = target_accuracy*gq["inf_sup_constant"] / \ (2*4 * gq["continuity_constant"]) / (ldict["csi"]*ldict["Psi_norm"]) local_failure_tolerance = max_failure_probability / ((gq["coarse_grid_resolution"] - 1)**2.) basis = adaptive_rrf(transop, ldict["source_product"], product, tol_i, local_failure_tolerance, num_testvecs, True) basis.append(lsol) gram_schmidt(basis, product, copy=False) bases[space] = basis return bases
def rrf(A, source_product=None, range_product=None, q=2, l=8, iscomplex=False): """Randomized range approximation of `A`. This is an implementation of Algorithm 4.4 in [HMT11]_. Given the |Operator| `A`, the return value of this method is the |VectorArray| `Q` whose vectors form an approximate orthonomal basis for the range of `A`. Parameters ---------- A The |Operator| A. source_product Inner product |Operator| of the source of A. range_product Inner product |Operator| of the range of A. q The number of power iterations. l The block size of the normalized power iterations. iscomplex If `True`, the random vectors are chosen complex. Returns ------- Q |VectorArray| which contains the basis, whose span approximates the range of A. """ assert source_product is None or isinstance(source_product, OperatorInterface) assert range_product is None or isinstance(range_product, OperatorInterface) assert isinstance(A, OperatorInterface) R = A.source.random(l, distribution='normal') if iscomplex: R += 1j * A.source.random(l, distribution='normal') Q = A.apply(R) gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False) for i in range(q): Q = A.apply_adjoint(Q) gram_schmidt(Q, source_product, atol=0, rtol=0, copy=False) Q = A.apply(Q) gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False) return Q
def rrf(A, source_product=None, range_product=None, q=2, l=8, iscomplex=False): """Randomized range approximation of `A`. This is an implementation of Algorithm 4.4 in [HMT11]_. Given the |Operator| `A`, the return value of this method is the |VectorArray| `Q` whose vectors form an approximate orthonomal basis for the range of `A`. Parameters ---------- A The |Operator| A. source_product Inner product |Operator| of the source of A. range_product Inner product |Operator| of the range of A. q The number of power iterations. l The block size of the normalized power iterations. iscomplex If `True`, the random vectors are chosen complex. Returns ------- Q |VectorArray| which contains the basis, whose span approximates the range of A. """ assert source_product is None or isinstance(source_product, OperatorInterface) assert range_product is None or isinstance(range_product, OperatorInterface) assert isinstance(A, OperatorInterface) R = A.source.random(l, distribution='normal') if iscomplex: R += 1j*A.source.random(l, distribution='normal') Q = A.apply(R) gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False) for i in range(q): Q = A.apply_adjoint(Q) gram_schmidt(Q, source_product, atol=0, rtol=0, copy=False) Q = A.apply(Q) gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False) return Q
def ei_greedy( U, error_norm=None, target_error=None, max_interpolation_dofs=None, projection="orthogonal", product=None ): """Generate data for empirical interpolation by a greedy search (EI-Greedy algorithm). Given a |VectorArray| `U`, this method generates a collateral basis and interpolation DOFs for empirical interpolation of the vectors contained in `U`. The returned objects can also be used to instantiate an |EmpiricalInterpolatedOperator|. The interpolation data is generated by a greedy search algorithm, adding in each loop the worst approximated vector in `U` to the collateral basis. Parameters ---------- U A |VectorArray| of vectors to interpolate. error_norm Norm w.r.t. which to calculate the interpolation error. If `None`, the Euclidean norm is used. target_error Stop the greedy search if the largest approximation error is below this threshold. max_interpolation_dofs Stop the greedy search if the number of interpolation DOF (= dimension of the collateral basis) reaches this value. projection If `ei`, compute the approximation error by comparing the given vector by its interpolant. If `orthogonal`, compute the error by comparing with the orthogonal projection onto the span of the collateral basis. product If `projection == 'orthogonal'`, the product which is used to perform the projection. If `None`, the Euclidean product is used. Returns ------- interpolation_dofs |NumPy array| of the DOFs at which the vectors are evaluated. collateral_basis |VectorArray| containing the generated collateral basis. data Dict containing the following fields: :errors: Sequence of maximum approximation errors during greedy search. :triangularity_errors: Sequence of maximum absolute values of interoplation matrix coefficients in the upper triangle (should be near zero). """ assert projection in ("orthogonal", "ei") assert isinstance(U, VectorArrayInterface) logger = getLogger("pymor.algorithms.ei.ei_greedy") logger.info("Generating Interpolation Data ...") interpolation_dofs = np.zeros((0,), dtype=np.int32) collateral_basis = U.empty() max_errs = [] triangularity_errs = [] if projection == "orthogonal": ERR = U.copy() onb_collateral_basis = collateral_basis.empty() else: ERR = U # main loop while True: errs = ERR.l2_norm() if error_norm is None else error_norm(ERR) max_err_ind = np.argmax(errs) max_err = errs[max_err_ind] if len(interpolation_dofs) >= max_interpolation_dofs: logger.info("Maximum number of interpolation DOFs reached. Stopping extension loop.") logger.info( "Final maximum {} error with {} interpolation DOFs: {}".format( "projection" if projection else "interpolation", len(interpolation_dofs), max_err ) ) break logger.info( "Maximum {} error with {} interpolation DOFs: {}".format( "projection" if projection else "interpolation", len(interpolation_dofs), max_err ) ) if target_error is not None and max_err <= target_error: logger.info("Target error reached! Stopping extension loop.") break # compute new interpolation dof and collateral basis vector new_vec = U.copy(ind=max_err_ind) new_dof = new_vec.amax()[0][0] if new_dof in interpolation_dofs: logger.info("DOF {} selected twice for interplation! Stopping extension loop.".format(new_dof)) break new_dof_value = new_vec.components([new_dof])[0, 0] if new_dof_value == 0.0: logger.info( "DOF {} selected for interpolation has zero maximum error! Stopping extension loop.".format(new_dof) ) break new_vec *= 1 / new_dof_value interpolation_dofs = np.hstack((interpolation_dofs, new_dof)) collateral_basis.append(new_vec) max_errs.append(max_err) # update U and ERR new_dof_values = U.components([new_dof]) U.axpy(-new_dof_values[:, 0], new_vec) if projection == "orthogonal": onb_collateral_basis.append(new_vec) gram_schmidt(onb_collateral_basis, offset=len(onb_collateral_basis) - 1, copy=False) coeffs = ERR.dot(onb_collateral_basis, o_ind=len(onb_collateral_basis) - 1) ERR.axpy(-coeffs[:, 0], onb_collateral_basis, x_ind=len(onb_collateral_basis) - 1) interpolation_matrix = collateral_basis.components(interpolation_dofs).T triangularity_errors = np.abs(interpolation_matrix - np.tril(interpolation_matrix)) for d in range(1, len(interpolation_matrix) + 1): triangularity_errs.append(np.max(triangularity_errors[:d, :d])) logger.info("Interpolation matrix is not lower triangular with maximum error of {}".format(triangularity_errs[-1])) logger.info("") data = {"errors": max_errs, "triangularity_errors": triangularity_errs} return interpolation_dofs, collateral_basis, data
def extend_basis(self, U, method='gram_schmidt', pod_modes=1, pod_orthonormalize=True, copy_U=True): """Extend basis by new vectors. Parameters ---------- U |VectorArray| containing the new basis vectors. method Basis extension method to use. The following methods are available: :trivial: Vectors in `U` are appended to the basis. Duplicate vectors in the sense of :func:`~pymor.algorithms.basic.almost_equal` are removed. :gram_schmidt: New basis vectors are orthonormalized w.r.t. to the old basis using the :func:`~pymor.algorithms.gram_schmidt.gram_schmidt` algorithm. :pod: Append the first POD modes of the defects of the projections of the vectors in U onto the existing basis (e.g. for use in POD-Greedy algorithm). .. warning:: In case of the `'gram_schmidt'` and `'pod'` extension methods, the existing reduced basis is assumed to be orthonormal w.r.t. the given inner product. pod_modes In case `method == 'pod'`, the number of POD modes that shall be appended to the basis. pod_orthonormalize If `True` and `method == 'pod'`, re-orthonormalize the new basis vectors obtained by the POD in order to improve numerical accuracy. copy_U If `copy_U` is `False`, the new basis vectors might be removed from `U`. Raises ------ ExtensionError Raised when the selected extension method does not yield a basis of increased dimension. """ assert method in ('trivial', 'gram_schmidt', 'pod') basis_length = len(self.RB) if method == 'trivial': remove = set() for i in range(len(U)): if np.any(almost_equal(U[i], self.RB)): remove.add(i) self.RB.append(U[[i for i in range(len(U)) if i not in remove]], remove_from_other=(not copy_U)) elif method == 'gram_schmidt': self.RB.append(U, remove_from_other=(not copy_U)) gram_schmidt(self.RB, offset=basis_length, product=self.product, copy=False) elif method == 'pod': if self.product is None: U_proj_err = U - self.RB.lincomb(U.dot(self.RB)) else: U_proj_err = U - self.RB.lincomb(self.product.apply2(U, self.RB)) self.RB.append(pod(U_proj_err, modes=pod_modes, product=self.product, orthonormalize=False)[0]) if pod_orthonormalize: gram_schmidt(self.RB, offset=basis_length, product=self.product, copy=False) if len(self.RB) <= basis_length: raise ExtensionError
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 ------- rd Reduced system. """ assert 0 < r < self.d.n assert projection in ('sr', 'bfsr', 'biorth') # compute all necessary Gramian factors pcf = self.d.gramian('pcf') pof = self.d.gramian('pof') vcf = self.d.gramian('vcf') vof = self.d.gramian('vof') 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)) Up = Up.T Uv, sv, Vv = spla.svd(vof.inner(vcf, product=self.d.M)) 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, self.d.state_space.id))} elif projection == 'bfsr': self.V1 = gram_schmidt(self.V1, atol=0, rtol=0) self.W1 = gram_schmidt(self.W1, atol=0, rtol=0) self.V2 = gram_schmidt(self.V2, atol=0, rtol=0) self.W2 = gram_schmidt(self.W2, atol=0, rtol=0) W1TV1invW1TV2 = spla.solve(self.W1.inner(self.V1), self.W1.inner(self.V2)) projected_ops = {'M': project(self.d.M, range_basis=self.W2, source_basis=self.V2)} elif projection == 'biorth': self.V1, self.W1 = gram_schmidt_biorth(self.V1, self.W1) self.V2, self.W2 = gram_schmidt_biorth(self.V2, self.W2, product=self.d.M) W1TV1invW1TV2 = self.W1.inner(self.V2) projected_ops = {'M': IdentityOperator(NumpyVectorSpace(r, self.d.state_space.id))} projected_ops.update({'E': project(self.d.E, range_basis=self.W2, source_basis=self.V2), 'K': project(self.d.K, range_basis=self.W2, source_basis=self.V1.lincomb(W1TV1invW1TV2.T)), 'B': project(self.d.B, range_basis=self.W2, source_basis=None), 'Cp': project(self.d.Cp, range_basis=None, source_basis=self.V1.lincomb(W1TV1invW1TV2.T)), 'Cv': project(self.d.Cv, range_basis=None, source_basis=self.V2)}) rd = self.d.with_(operators=projected_ops, visualizer=None, estimator=None, cache_region=None, name=self.d.name + '_reduced') rd.disable_logging() return rd
def ei_greedy(U, error_norm=None, atol=None, rtol=None, max_interpolation_dofs=None, projection='ei', product=None, copy=True, pool=dummy_pool): """Generate data for empirical interpolation using EI-Greedy algorithm. Given a |VectorArray| `U`, this method generates a collateral basis and interpolation DOFs for empirical interpolation of the vectors contained in `U`. The returned objects can be used to instantiate an |EmpiricalInterpolatedOperator| (with `triangular=True`). The interpolation data is generated by a greedy search algorithm, where in each loop iteration the worst approximated vector in `U` is added to the collateral basis. Parameters ---------- U A |VectorArray| of vectors to interpolate. error_norm Norm w.r.t. which to calculate the interpolation error. If `None`, the Euclidean norm is used. atol Stop the greedy search if the largest approximation error is below this threshold. rtol Stop the greedy search if the largest relative approximation error is below this threshold. max_interpolation_dofs Stop the greedy search if the number of interpolation DOF (= dimension of the collateral basis) reaches this value. projection If `'ei'`, compute the approximation error by comparing the given vectors by their interpolants. If `'orthogonal'`, compute the error by comparing with the orthogonal projection onto the span of the collateral basis. product If `projection == 'orthogonal'`, the inner product which is used to perform the projection. If `None`, the Euclidean product is used. copy If `False`, `U` will be modified during executing of the algorithm. pool If not `None`, the |WorkerPool| to use for parallelization. Returns ------- interpolation_dofs |NumPy array| of the DOFs at which the vectors are evaluated. collateral_basis |VectorArray| containing the generated collateral basis. data Dict containing the following fields: :errors: Sequence of maximum approximation errors during greedy search. :triangularity_errors: Sequence of maximum absolute values of interoplation matrix coefficients in the upper triangle (should be near zero). """ assert projection in ('orthogonal', 'ei') if pool: # dispatch to parallel implemenation if projection == 'ei': pass elif projection == 'orthogonal': raise ValueError('orthogonal projection not supported in parallel implementation') else: assert False assert isinstance(U, (VectorArrayInterface, RemoteObjectInterface)) with RemoteObjectManager() as rom: if isinstance(U, VectorArrayInterface): U = rom.manage(pool.scatter_array(U)) return _parallel_ei_greedy(U, error_norm=error_norm, atol=atol, rtol=rtol, max_interpolation_dofs=max_interpolation_dofs, copy=copy, pool=pool) assert isinstance(U, VectorArrayInterface) logger = getLogger('pymor.algorithms.ei.ei_greedy') logger.info('Generating Interpolation Data ...') interpolation_dofs = np.zeros((0,), dtype=np.int32) collateral_basis = U.empty() max_errs = [] triangularity_errs = [] if copy: U = U.copy() if projection == 'orthogonal': ERR = U.copy() onb_collateral_basis = collateral_basis.empty() else: ERR = U errs = ERR.l2_norm() if error_norm is None else error_norm(ERR) max_err_ind = np.argmax(errs) initial_max_err = max_err = errs[max_err_ind] # main loop while True: if max_interpolation_dofs is not None and len(interpolation_dofs) >= max_interpolation_dofs: logger.info('Maximum number of interpolation DOFs reached. Stopping extension loop.') logger.info('Final maximum {} error with {} interpolation DOFs: {}'.format( 'projection' if projection else 'interpolation', len(interpolation_dofs), max_err)) break logger.info('Maximum {} error with {} interpolation DOFs: {}' .format('projection' if projection else 'interpolation', len(interpolation_dofs), max_err)) if atol is not None and max_err <= atol: logger.info('Absolute error tolerance reached! Stopping extension loop.') break if rtol is not None and max_err / initial_max_err <= rtol: logger.info('Relative error tolerance reached! Stopping extension loop.') break # compute new interpolation dof and collateral basis vector new_vec = U.copy(ind=max_err_ind) new_dof = new_vec.amax()[0][0] if new_dof in interpolation_dofs: logger.info('DOF {} selected twice for interplation! Stopping extension loop.'.format(new_dof)) break new_dof_value = new_vec.components([new_dof])[0, 0] if new_dof_value == 0.: logger.info('DOF {} selected for interpolation has zero maximum error! Stopping extension loop.' .format(new_dof)) break new_vec *= 1 / new_dof_value interpolation_dofs = np.hstack((interpolation_dofs, new_dof)) collateral_basis.append(new_vec) max_errs.append(max_err) # update U and ERR new_dof_values = U.components([new_dof]) U.axpy(-new_dof_values[:, 0], new_vec) if projection == 'orthogonal': onb_collateral_basis.append(new_vec) gram_schmidt(onb_collateral_basis, offset=len(onb_collateral_basis) - 1, copy=False) coeffs = ERR.dot(onb_collateral_basis, o_ind=len(onb_collateral_basis) - 1) ERR.axpy(-coeffs[:, 0], onb_collateral_basis, x_ind=len(onb_collateral_basis) - 1) errs = ERR.l2_norm() if error_norm is None else error_norm(ERR) max_err_ind = np.argmax(errs) max_err = errs[max_err_ind] interpolation_matrix = collateral_basis.components(interpolation_dofs).T triangularity_errors = np.abs(interpolation_matrix - np.tril(interpolation_matrix)) for d in range(1, len(interpolation_matrix) + 1): triangularity_errs.append(np.max(triangularity_errors[:d, :d])) if len(triangularity_errs) > 0: logger.info('Interpolation matrix is not lower triangular with maximum error of {}' .format(triangularity_errs[-1])) data = {'errors': max_errs, 'triangularity_errors': triangularity_errs} return interpolation_dofs, collateral_basis, data
def estimate_image(operators=(), vectors=(), domain=None, extends=False, orthonormalize=True, product=None, riesz_representatives=False): """Estimate the image of given operators for all mu. Let `operators` be a list of |Operators| with common source and domain, and let `vectors` be a list of |VectorArrays| or vector-like |Operators| in the range of these operators. Given a |VectorArray| `domain` of vectors in the source of the operators, this algorithms determines a |VectorArray| `image` of range vectors such that the linear span of `image` contains: - `op.apply(U, mu=mu)` for all operators `op` in `operators`, for all possible |Parameters| `mu` and for all |VectorArrays| `U` contained in the linear span of `domain`, - `U` for all |VectorArrays| in `vectors`, - `v.as_vector(mu)` for all |Operators| in `vectors` and all possible |Parameters| `mu`. The algorithm will try to choose `image` as small as possible. However, no optimality is guaranteed. Parameters ---------- operators See above. vectors See above. domain See above. If `None`, an empty `domain` |VectorArray| is assumed. extends For some operators, e.g. |EmpiricalInterpolatedOperator|, as well as for all elements of `vectors`, `image` is estimated independently from the choice of `domain`. If `extends` is `True`, such operators are ignored. (This is useful in case these vectors have already been obtained by earlier calls to this function.) orthonormalize Compute an orthonormal basis for the linear span of `image` using the :func:`~pymor.algorithms.gram_schmidt.gram_schmidt` algorithm. product Inner product |Operator| w.r.t. which to orthonormalize. riesz_representatives If `True`, compute Riesz representatives of the vectors in `image` before orthonormalizing. (Useful for norm computation when the range of the `operators` is a dual space.) Returns ------- The |VectorArray| `image`. Raises ------ ImageCollectionError Is raised when for a given |Operator| no image estimate is possible. """ assert operators or vectors domain_space = operators[0].source if operators else None image_space = operators[0].range if operators \ else vectors[0].space if isinstance(vectors[0], VectorArrayInterface) \ else vectors[0].range if vectors[0].range != NumpyVectorSpace(1) \ else vectors[0].source assert all( isinstance(v, VectorArrayInterface) and v in image_space or v.source == NumpyVectorSpace(1) and v.range == image_space and v.linear or v.range == NumpyVectorSpace(1) and v.source == image_space and v.linear for v in vectors ) assert all(op.source == domain_space and op.range == image_space for op in operators) assert domain is None or domain_space is None or domain in domain_space assert product is None or product.source == product.range == image_space def collect_operator_ranges(op, source, image): if isinstance(op, (LincombOperator, SelectionOperator)): for o in op.operators: collect_operator_ranges(o, source, image) elif isinstance(op, EmpiricalInterpolatedOperator): if hasattr(op, 'collateral_basis') and not extends: image.append(op.collateral_basis) elif isinstance(op, Concatenation): firstrange = op.first.range.empty() collect_operator_ranges(op.first, source, firstrange) collect_operator_ranges(op.second, firstrange, image) elif op.linear and not op.parametric: image.append(op.apply(source)) else: raise ImageCollectionError(op) def collect_vector_ranges(op, image): if isinstance(op, (LincombOperator, SelectionOperator)): for o in op.operators: collect_vector_ranges(o, image) elif isinstance(op, AdjointOperator): if op.source not in image_space: raise ImageCollectionError(op) # Not implemented operator = Concatenation(op.range_product, op.operator) if op.range_product else op.operator collect_operator_ranges(operator, NumpyVectorArray(np.ones(1)), image) elif op.linear and not op.parametric: image.append(op.as_vector()) else: raise ImageCollectionError(op) image = image_space.empty() if not extends: for f in vectors: collect_vector_ranges(f, image) if operators and domain is None: domain = domain_space.empty() for op in operators: collect_operator_ranges(op, domain, image) if riesz_representatives and product: image = product.apply_inverse(image) if orthonormalize: gram_schmidt(image, product=product, copy=False) return image
def estimate_image_hierarchical(operators=(), vectors=(), domain=None, extends=None, orthonormalize=True, product=None, riesz_representatives=False): """Estimate the image of given operators for all mu. This is an extended version of :func:`estimate_image`, which calls :func:`estimate_image` individually for each vector of `domain`. As a result, the vectors in the returned `image` |VectorArray| will be ordered by the `domain` vector they correspond to (starting with vectors which correspond to the `functionals` and to the |Operators| for which the image is estimated independently from `domain`). This function also returns an `image_dims` list, such that the first `image_dims[i+1]` vectors of `image` correspond to the first `i` vectors of `domain` (the first `image_dims[0]` vectors correspond to `vectors` and to the |Operators| with fixed image estimate). Parameters ---------- operators See :func:`estimate_image`. vectors See :func:`estimate_image`. domain See :func:`estimate_image`. extends When additional vectors have been appended to the `domain` |VectorArray| after :func:`estimate_image_hierarchical` has been called, and :func:`estimate_image_hierarchical` shall be called again for the extended `domain` array, `extends` can be set to `(image, image_dims)`, where `image`, `image_dims` are the return values of the last :func:`estimate_image_hierarchical` call. The old `domain` vectors will then be skipped during computation and `image`, `image_dims` will be modified in-place. orthonormalize See :func:`estimate_image`. product See :func:`estimate_image`. riesz_representatives See :func:`estimate_image`. Returns ------- image See above. image_dims See above. Raises ------ ImageCollectionError Is raised when for a given |Operator| no image estimate is possible. """ assert operators or vectors domain_space = operators[0].source if operators else None image_space = operators[0].range if operators \ else vectors[0].space if isinstance(vectors[0], VectorArrayInterface) \ else vectors[0].range if vectors[0].range != NumpyVectorSpace(1) \ else vectors[0].source assert all( isinstance(v, VectorArrayInterface) and v in image_space or v.source == NumpyVectorSpace(1) and v.range == image_space and v.linear or v.range == NumpyVectorSpace(1) and v.source == image_space and v.linear for v in vectors ) assert all(op.source == domain_space and op.range == image_space for op in operators) assert domain is None or domain_space is None or domain in domain_space assert product is None or product.source == product.range == image_space assert extends is None or len(extends) == 2 logger = getLogger('pymor.algorithms.image.estimate_image_hierarchical') if operators and domain is None: domain = domain_space.empty() if extends: image = extends[0] image_dims = extends[1] ind_range = list(range(len(image_dims) - 1, len(domain))) if operators else list(range(len(image_dims) - 1, 0)) else: image = image_space.empty() image_dims = [] ind_range = list(range(-1, len(domain))) if operators else [-1] for i in ind_range: logger.info('Estimating image for basis vector {} ...'.format(i)) if i == -1: new_image = estimate_image(operators, vectors, None, extends=False, orthonormalize=False, product=product, riesz_representatives=riesz_representatives) else: new_image = estimate_image(operators, [], domain.copy(i), extends=True, orthonormalize=False, product=product, riesz_representatives=riesz_representatives) gram_schmidt_offset = len(image) image.append(new_image, remove_from_other=True) if orthonormalize: with logger.block('Orthonormalizing ...'): gram_schmidt(image, offset=gram_schmidt_offset, product=product, copy=False) image_dims.append(len(image)) return image, image_dims
def projection_shifts(A, E, Z, W, prev_shifts, shift_options): """Find further shift parameters for low-rank ADI iteration using Galerkin projection on spaces spanned by LR-ADI iterates. See [PK16]_, pp. 92-95. Parameters ---------- A The |Operator| A from the corresponding Lyapunov equation. E The |Operator| E from the corresponding Lyapunov equation. Z A |VectorArray| representing the currently computed low-rank solution factor. W A |VectorArray| representing the currently computed low-rank residual factor. prev_shifts A |NumPy array| containing the set of all previously used shift parameters. shift_options The shift options to use (see :func:`lyap_solver_options`). Returns ------- shifts A |NumPy array| containing a set of stable shift parameters. """ u = shift_options['z_columns'] L = prev_shifts.size r = len(W) d = L - u if d < 0: u = L d = 0 if prev_shifts[-u].imag < 0: u = u + 1 Vu = Z[-u * r:] # last u matrices V added to solution factor Z if shift_options['implicit_subspace']: B = np.zeros((u, u)) G = np.zeros((u, 1)) Ir = np.eye(r) iC = np.where(np.imag(prev_shifts) > 0)[0] # complex shifts indices (first shift of complex pair) iR = np.where(np.isreal(prev_shifts))[0] # real shifts indices iC = iC[iC >= d] iR = iR[iR >= d] i = 0 while i < u: rS = iR[iR < d + i] cS = iC[iC < d + i] rp = prev_shifts[d + i].real cp = prev_shifts[d + i].imag G[i, 0] = np.sqrt(-2 * rp) if cp == 0: B[i, i] = rp if rS.size > 0: B[i, rS - d] = -2 * np.sqrt(rp*np.real(prev_shifts[rS])) if cS.size > 0: B[i, cS - d] = -2 * np.sqrt(2*rp*np.real(prev_shifts[cS])) i = i + 1 else: sri = np.sqrt(rp**2+cp**2) B[i: i + 2, i: i + 2] = [[2*rp, -sri], [sri, 0]] if rS.size > 0: B[i, rS - d] = -2 * np.sqrt(2*rp*np.real(prev_shifts[rS])) if cS.size > 0: B[i, cS - d] = -4 * np.sqrt(rp*np.real(prev_shifts[cS])) i = i + 2 B = spla.kron(B, Ir) G = spla.kron(G, Ir) s, v = spla.svd(Vu.gramian(), full_matrices=False)[1:3] P = v.T.dot(np.diag(1. / np.sqrt(s))) Q = Vu.to_numpy().T.dot(P) E_V = E.apply(Vu).to_numpy().T T = Q.T.dot(E_V) Ap = Q.T.dot(W.to_numpy().T).dot(G.T).dot(P) + T.dot(B.dot(P)) Ep = T.dot(P) else: Q = gram_schmidt(Vu, atol=0, rtol=0) Ap = A.apply2(Q, Q) Ep = E.apply2(Q, Q) shifts = spla.eigvals(Ap, Ep) shifts = shifts[np.real(shifts) < 0] if shifts.size == 0: return np.concatenate((prev_shifts, prev_shifts)) else: return np.concatenate((prev_shifts, shifts))
def estimate_image(operators=(), vectors=(), domain=None, extends=False, orthonormalize=True, product=None, riesz_representatives=False): """Estimate the image of given |Operators| for all mu. Let `operators` be a list of |Operators| with common source and range, and let `vectors` be a list of |VectorArrays| or vector-like |Operators| in the range of these operators. Given a |VectorArray| `domain` of vectors in the source of the operators, this algorithms determines a |VectorArray| `image` of range vectors such that the linear span of `image` contains: - `op.apply(U, mu=mu)` for all operators `op` in `operators`, for all possible |Parameters| `mu` and for all |VectorArrays| `U` contained in the linear span of `domain`, - `U` for all |VectorArrays| in `vectors`, - `v.as_range_array(mu)` for all |Operators| in `vectors` and all possible |Parameters| `mu`. The algorithm will try to choose `image` as small as possible. However, no optimality is guaranteed. The image estimation algorithm is specified by :class:`CollectOperatorRangeRules` and :class:`CollectVectorRangeRules`. Parameters ---------- operators See above. vectors See above. domain See above. If `None`, an empty `domain` |VectorArray| is assumed. extends For some operators, e.g. |EmpiricalInterpolatedOperator|, as well as for all elements of `vectors`, `image` is estimated independently from the choice of `domain`. If `extends` is `True`, such operators are ignored. (This is useful in case these vectors have already been obtained by earlier calls to this function.) orthonormalize Compute an orthonormal basis for the linear span of `image` using the :func:`~pymor.algorithms.gram_schmidt.gram_schmidt` algorithm. product Inner product |Operator| w.r.t. which to orthonormalize. riesz_representatives If `True`, compute Riesz representatives of the vectors in `image` before orthonormalizing (useful for dual norm computation when the range of the `operators` is a dual space). Returns ------- The |VectorArray| `image`. Raises ------ ImageCollectionError Is raised when for a given |Operator| no image estimate is possible. """ assert operators or vectors domain_space = operators[0].source if operators else None image_space = operators[0].range if operators \ else vectors[0].space if isinstance(vectors[0], VectorArrayInterface) \ else vectors[0].range assert all(op.source == domain_space and op.range == image_space for op in operators) assert all( isinstance(v, VectorArrayInterface) and ( v in image_space ) or isinstance(v, OperatorInterface) and ( v.range == image_space and isinstance(v.source, NumpyVectorSpace) and v.linear ) for v in vectors ) assert domain is None or domain_space is None or domain in domain_space assert product is None or product.source == product.range == image_space image = image_space.empty() if not extends: rules = CollectVectorRangeRules(image) for v in vectors: try: rules.apply(v) except NoMatchingRuleError as e: raise ImageCollectionError(e.obj) if operators and domain is None: domain = domain_space.empty() for op in operators: rules = CollectOperatorRangeRules(domain, image, extends) try: rules.apply(op) except NoMatchingRuleError as e: raise ImageCollectionError(e.obj) if riesz_representatives and product: image = product.apply_inverse(image) if orthonormalize: gram_schmidt(image, product=product, copy=False) return image
def adaptive_rrf(A, source_product=None, range_product=None, tol=1e-4, failure_tolerance=1e-15, num_testvecs=20, lambda_min=None, iscomplex=False): r"""Adaptive randomized range approximation of `A`. This is an implementation of Algorithm 1 in [BS18]_. Given the |Operator| `A`, the return value of this method is the |VectorArray| `B` with the property .. math:: \Vert A - P_{span(B)} A \Vert \leq tol with a failure probability smaller than `failure_tolerance`, where the norm denotes the operator norm. The inner product of the range of `A` is given by `range_product` and the inner product of the source of `A` is given by `source_product`. Parameters ---------- A The |Operator| A. source_product Inner product |Operator| of the source of A. range_product Inner product |Operator| of the range of A. tol Error tolerance for the algorithm. failure_tolerance Maximum failure probability. num_testvecs Number of test vectors. lambda_min The smallest eigenvalue of source_product. If `None`, the smallest eigenvalue is computed using scipy. iscomplex If `True`, the random vectors are chosen complex. Returns ------- B |VectorArray| which contains the basis, whose span approximates the range of A. """ assert source_product is None or isinstance(source_product, OperatorInterface) assert range_product is None or isinstance(range_product, OperatorInterface) assert isinstance(A, OperatorInterface) B = A.range.empty() R = A.source.random(num_testvecs, distribution='normal') if iscomplex: R += 1j*A.source.random(num_testvecs, distribution='normal') if source_product is None: lambda_min = 1 elif lambda_min is None: def mv(v): return source_product.apply(source_product.source.from_numpy(v)).to_numpy() def mvinv(v): return source_product.apply_inverse(source_product.range.from_numpy(v)).to_numpy() L = LinearOperator((source_product.source.dim, source_product.range.dim), matvec=mv) Linv = LinearOperator((source_product.range.dim, source_product.source.dim), matvec=mvinv) lambda_min = eigsh(L, sigma=0, which="LM", return_eigenvectors=False, k=1, OPinv=Linv)[0] testfail = failure_tolerance / min(A.source.dim, A.range.dim) testlimit = np.sqrt(2. * lambda_min) * erfinv(testfail**(1. / num_testvecs)) * tol maxnorm = np.inf M = A.apply(R) while(maxnorm > testlimit): basis_length = len(B) v = A.source.random(distribution='normal') if iscomplex: v += 1j*A.source.random(distribution='normal') B.append(A.apply(v)) gram_schmidt(B, range_product, atol=0, rtol=0, offset=basis_length, copy=False) M -= B.lincomb(B.inner(M, range_product).T) maxnorm = np.max(M.norm(range_product)) return B
def arnoldi(A, E, b, sigma, trans=False): r"""Rational Arnoldi algorithm. If `trans == False`, using Arnoldi process, computes a real orthonormal basis for the rational Krylov subspace .. math:: \mathrm{span}\{(\sigma_1 E - A)^{-1} b, (\sigma_2 E - A)^{-1} b, \ldots, (\sigma_r E - A)^{-1} b\}, otherwise, computes the same for .. math:: \mathrm{span}\{(\sigma_1 E - A)^{-T} b^T, (\sigma_2 E - A)^{-T} b^T, \ldots, (\sigma_r E - A)^{-T} b^T\}. Interpolation points in `sigma` are allowed to repeat (in any order). Then, in the above expression, .. math:: \underbrace{(\sigma_i E - A)^{-1} b, \ldots, (\sigma_i E - A)^{-1} b}_{m \text{ times}} is replaced by .. math:: (\sigma_i E - A)^{-1} b, (\sigma_i E - A)^{-2} b, \ldots, (\sigma_i E - A)^{-m} b. Analogously for the `trans == True` case. Parameters ---------- A Real |Operator| A. E Real |Operator| E. b Real vector-like operator (if trans is False) or functional (if trans is True). sigma Interpolation points (closed under conjugation). trans Boolean, see above. Returns ------- V Projection matrix. """ assert not trans and b.source.dim == 1 or trans and b.range.dim == 1 r = len(sigma) V = A.source.empty(reserve=r) v = b.as_vector() v.scal(1 / v.l2_norm()[0]) for i in range(r): if sigma[i].imag == 0: sEmA = sigma[i].real * E - A if not trans: v = sEmA.apply_inverse(v) else: v = sEmA.apply_inverse_adjoint(v) V.append(v) V = gram_schmidt(V, atol=0, rtol=0, offset=len(V) - 1, copy=False) v = V[-1] elif sigma[i].imag > 0: sEmA = sigma[i] * E - A if not trans: v = sEmA.apply_inverse(v) else: v = sEmA.apply_inverse_adjoint(v) V.append(v.real) V.append(v.imag) V = gram_schmidt(V, atol=0, rtol=0, offset=len(V) - 2, copy=False) v = V[-1] return V
def pod_basis_extension(basis, U, count=1, copy_basis=True, product=None, orthonormalize=True): """Extend basis with the first `count` POD modes of the projection of `U` onto the orthogonal complement of the basis. Note that the provided basis is assumed to be orthonormal w.r.t. the provided scalar product! Parameters ---------- basis |VectorArray| containing the basis to extend. The basis is expected to be orthonormal w.r.t. `product`. U |VectorArray| containing the vectors to which the POD is applied. count Number of POD modes that are to be appended to the basis. product The scalar product w.r.t. which to orthonormalize; if `None`, the Euclidean product is used. copy_basis If `copy_basis` is `False`, the old basis is extended in-place. orthonormalize If `True`, re-orthonormalize the new basis vectors obtained by the POD in order to improve numerical accuracy. Returns ------- new_basis The extended basis. extension_data Dict containing the following fields: :hierarchic: `True` if `new_basis` contains `basis` as its first vectors. Raises ------ ExtensionError POD produces no new vectors. This is the case when no vector in `U` is linearly independent from the basis. """ if basis is None: return pod(U, modes=count, product=product)[0], {'hierarchic': True} basis_length = len(basis) new_basis = basis.copy() if copy_basis else basis if product is None: U_proj_err = U - basis.lincomb(U.dot(basis)) else: U_proj_err = U - basis.lincomb(product.apply2(U, basis)) new_basis.append(pod(U_proj_err, modes=count, product=product, orthonormalize=False)[0]) if orthonormalize: gram_schmidt(new_basis, offset=len(basis), product=product, copy=False) if len(new_basis) <= basis_length: raise ExtensionError return new_basis, {'hierarchic': True}
def reduce_residual(operator, functional=None, RB=None, product=None, extends=None): """Generic reduced basis residual reductor. Given an operator and a functional, the concatenation of residual operator with the Riesz isomorphism is given by:: riesz_residual.apply(U, mu) == product.apply_inverse(operator.apply(U, mu) - functional.as_vector(mu)) This reductor determines a low-dimensional subspace of image of a reduced basis space under `riesz_residual`, computes an orthonormal basis `residual_range` of this range spaces and then returns the Petrov-Galerkin projection :: projected_riesz_residual === riesz_residual.projected(range_basis=residual_range, source_basis=RB) of the `riesz_residual` operator. Given an reduced basis coefficient vector `u`, the dual norm of the residual can then be computed as :: projected_riesz_residual.apply(u, mu).l2_norm() Moreover, a `reconstructor` is provided such that :: reconstructor.reconstruct(projected_riesz_residual.apply(u, mu)) == riesz_residual.apply(RB.lincomb(u), mu) Parameters ---------- operator See definition of `riesz_residual`. functional See definition of `riesz_residual`. If `None`, zero right-hand side is assumed. RB |VectorArray| containing a basis of the reduced space onto which to project. product Scalar product |Operator| w.r.t. which to compute the Riesz representatives. extends Set by :meth:`~pymor.algorithms.greedy.greedy` to the result of the last reduction in case the basis extension was `hierarchic`. Used to prevent re-computation of `residual_range` basis vectors already obtained from previous reductions. Returns ------- projected_riesz_residual See above. reconstructor See above. reduction_data Additional data produced by the reduction process. (Compare the `extends` parameter.) """ assert functional is None \ or functional.range == NumpyVectorSpace(1) and functional.source == operator.source and functional.linear assert RB is None or RB in operator.source assert product is None or product.source == product.range == operator.range assert extends is None or len(extends) == 3 logger = getLogger('pymor.reductors.reduce_residual') if RB is None: RB = operator.source.empty() if extends and isinstance(extends[0], NonProjectedResidualOperator): extends = None if extends: residual_range = extends[1].RB.copy() residual_range_dims = list(extends[2]['residual_range_dims']) ind_range = range(extends[0].source.dim, len(RB)) else: residual_range = operator.range.empty() residual_range_dims = [] ind_range = range(-1, len(RB)) class CollectionError(Exception): def __init__(self, op): super(CollectionError, self).__init__() self.op = op def collect_operator_ranges(op, source, ind, residual_range): if isinstance(op, (LincombOperator, SelectionOperator)): for o in op.operators: collect_operator_ranges(o, source, ind, residual_range) elif isinstance(op, EmpiricalInterpolatedOperator): if hasattr(op, 'collateral_basis') and ind == -1: residual_range.append(op.collateral_basis) elif isinstance(op, Concatenation): firstrange = op.first.range.empty() collect_operator_ranges(op.first, source, ind, firstrange) for j in range(len(firstrange)): collect_operator_ranges(op.second, firstrange, j, residual_range) elif op.linear and not op.parametric: if ind >= 0: residual_range.append(op.apply(source, ind=ind)) else: raise CollectionError(op) def collect_functional_ranges(op, residual_range): if isinstance(op, (LincombOperator, SelectionOperator)): for o in op.operators: collect_functional_ranges(o, residual_range) elif isinstance(op, AdjointOperator): operator = Concatenation(op.range_product, op.operator) if op.range_product else op.operator collect_operator_ranges(operator, NumpyVectorArray(np.ones(1)), 0, residual_range) elif op.linear and not op.parametric: residual_range.append(op.as_vector()) else: raise CollectionError(op) for i in ind_range: logger.info('Computing residual range for basis vector {}...'.format(i)) new_residual_range = operator.range.empty() try: if i == -1: collect_functional_ranges(functional, new_residual_range) collect_operator_ranges(operator, RB, i, new_residual_range) except CollectionError as e: logger.warn('Cannot compute range of {}. Evaluation will be slow.'.format(e.op)) operator = operator.projected(None, RB) return (NonProjectedResidualOperator(operator, functional, product), NonProjectedReconstructor(product), {}) if product: logger.info('Computing Riesz representatives for basis vector {}...'.format(i)) new_residual_range = product.apply_inverse(new_residual_range) gram_schmidt_offset = len(residual_range) residual_range.append(new_residual_range) logger.info('Orthonormalizing ...') gram_schmidt(residual_range, offset=gram_schmidt_offset, product=product, copy=False) residual_range_dims.append(len(residual_range)) logger.info('Projecting ...') operator = operator.projected(residual_range, RB, product=None) # the product always cancels out. functional = functional.projected(None, residual_range, product=None) return (ResidualOperator(operator, functional), GenericRBReconstructor(residual_range), {'residual_range_dims': residual_range_dims})
def pod(A, modes=None, product=None, tol=4e-8, symmetrize=False, orthonormalize=True, check=True, check_tol=1e-10): """Proper orthogonal decomposition of `A`. If the |VectorArray| `A` is viewed as a linear map :: A: R^(len(A)) ---> R^(dim(A)) then the return value of this method is simply the |VectorArray| of left-singular vectors of the singular value decomposition of `A` with the scalar product on R^(dim(A) given by `product` and the scalar product on R^(len(A)) being the Euclidean product. Parameters ---------- A The |VectorArray| for which the POD is to be computed. modes If not `None` only the first `modes` POD modes (singular vectors) are returned. products Scalar product |Operator| w.r.t. which the POD is computed. tol Singular values smaller than this value multiplied by the largest singular value are ignored. symmetrize If `True`, symmetrize the gramian again before proceeding. orthonormalize If `True`, orthonormalize the computed POD modes again using :func:`algorithms.gram_schmidt.gram_schmidt`. check If `True`, check the computed POD modes for orthonormality. check_tol Tolerance for the orthonormality check. Returns ------- POD |VectorArray| of POD modes. SVALS Sequence of singular values. """ assert isinstance(A, VectorArrayInterface) assert len(A) > 0 assert modes is None or modes <= len(A) assert product is None or isinstance(product, OperatorInterface) B = A.gramian() if product is None else product.apply2(A, A) if symmetrize: # according to rbmatlab this is necessary due to rounding B = B + B.T B *= 0.5 eigvals = None if modes is None else (len(B) - modes, len(B) - 1) EVALS, EVECS = eigh(B, overwrite_a=True, turbo=True, eigvals=eigvals) EVALS = EVALS[::-1] EVECS = EVECS.T[::-1, :] # is this a view? yes it is! above_tol = np.where(EVALS >= tol ** 2 * EVALS[0])[0] if len(above_tol) == 0: return type(A).empty(A.dim) last_above_tol = above_tol[-1] SVALS = np.sqrt(EVALS[:last_above_tol + 1]) EVECS = EVECS[:last_above_tol + 1] POD = A.lincomb(EVECS / SVALS[:, np.newaxis]) if orthonormalize: POD = gram_schmidt(POD, product=product, copy=False) if check: if not product and not float_cmp_all(POD.dot(POD), np.eye(len(POD)), atol=check_tol, rtol=0.): err = np.max(np.abs(POD.dot(POD) - np.eye(len(POD)))) raise AccuracyError('result not orthogonal (max err={})'.format(err)) elif product and not float_cmp_all(product.apply2(POD, POD), np.eye(len(POD)), atol=check_tol, rtol=0.): err = np.max(np.abs(product.apply2(POD, POD) - np.eye(len(POD)))) raise AccuracyError('result not orthogonal (max err={})'.format(err)) if len(POD) < len(EVECS): raise AccuracyError('additional orthonormalization removed basis vectors') return POD, SVALS
def reduce(self, sigma, b, c, projection='orth'): """Bitangential Hermite interpolation. Parameters ---------- sigma Interpolation points (closed under conjugation), list of length `r`. b Right tangential directions, |VectorArray| of length `r` from `self._B_source`. c Left tangential directions, |VectorArray| of length `r` from `self._C_range`. projection Projection method: - `'orth'`: projection matrices are orthogonalized with respect to the Euclidean inner product - `'biorth'`: projection matrices are biorthogolized with respect to the E product Returns ------- rd Reduced discretization. """ r = len(sigma) assert b in self._B_source and len(b) == r assert c in self._C_range and len(c) == r assert projection in ('orth', 'biorth') # rescale tangential directions (to avoid overflow or underflow) if b.dim > 1: b.scal(1 / b.l2_norm()) else: b = self._B_source.from_numpy(np.ones((r, 1))) if c.dim > 1: c.scal(1 / c.l2_norm()) else: c = self._C_range.from_numpy(np.ones((r, 1))) # compute projection matrices self.V = self._K_source.empty(reserve=r) self.W = self._K_source.empty(reserve=r) for i in range(r): if sigma[i].imag == 0: Bb = self._B_apply(sigma[i].real, b.real[i]) self.V.append(self._K_apply_inverse(sigma[i].real, Bb)) CTc = self._C_apply_adjoint(sigma[i].real, c.real[i]) self.W.append(self._K_apply_inverse_adjoint(sigma[i].real, CTc)) elif sigma[i].imag > 0: Bb = self._B_apply(sigma[i], b[i]) v = self._K_apply_inverse(sigma[i], Bb) self.V.append(v.real) self.V.append(v.imag) CTc = self._C_apply_adjoint(sigma[i], c[i].conj()) w = self._K_apply_inverse_adjoint(sigma[i], CTc) self.W.append(w.real) self.W.append(w.imag) if projection == 'orth': self.V = gram_schmidt(self.V, atol=0, rtol=0) self.W = gram_schmidt(self.W, atol=0, rtol=0) elif projection == 'biorth': self.V, self.W = gram_schmidt_biorth(self.V, self.W, product=self._product) self.pg_reductor = GenericPGReductor(self.d, self.W, self.V, projection == 'biorth', product=self._product) rd = self.pg_reductor.reduce() return rd