def _check_orthonormality(self, basis, offset=0): if not self.check_orthonormality or basis not in self.products: return U = self.bases[basis] product = self.products.get(basis, None) error_matrix = U[offset:].inner(U, product) error_matrix[:len(U) - offset, offset:] -= np.eye(len(U) - offset) if error_matrix.size > 0: err = np.max(np.abs(error_matrix)) if err >= self.check_tol: raise AccuracyError(f"result not orthogonal (max err={err})")
def pod(A, modes=None, product=None, rtol=4e-8, atol=0., l2_err=0., symmetrize=False, orthonormalize=True, check=True, check_tol=1e-10): """Proper orthogonal decomposition of `A`. Viewing the |VectorArray| `A` as a `A.dim` x `len(A)` matrix, the return value of this method is the |VectorArray| of left-singular vectors of the singular value decomposition of `A`, where the inner product on R^(`dim(A)`) is given by `product` and the inner product on R^(`len(A)`) is the Euclidean inner 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. product Inner product |Operator| w.r.t. which the POD is computed. rtol Singular values smaller than this value multiplied by the largest singular value are ignored. atol Singular values smaller than this value are ignored. l2_err Do not return more modes than needed to bound the l2-approximation error by this value. I.e. the number of returned modes is at most :: argmin_N { sum_{n=N+1}^{infty} s_n^2 <= l2_err^2 } where `s_n` denotes the n-th singular value. symmetrize If `True`, symmetrize the Gramian again before proceeding. orthonormalize If `True`, orthonormalize the computed POD modes again using the :func:`~pymor.algorithms.gram_schmidt.gram_schmidt` algorithm. 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) logger = getLogger('pymor.algorithms.pod.pod') with logger.block('Computing Gramian ({} vectors) ...'.format(len(A))): 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 with logger.block('Computing eigenvalue decomposition ...'): eigvals = None if (modes is None or l2_err > 0.) 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! tol = max(rtol**2 * EVALS[0], atol**2) above_tol = np.where(EVALS >= tol)[0] if len(above_tol) == 0: return A.space.empty(), np.array([]) last_above_tol = above_tol[-1] errs = np.concatenate((np.cumsum(EVALS[::-1])[::-1], [0.])) below_err = np.where(errs <= l2_err**2)[0] first_below_err = below_err[0] selected_modes = min(first_below_err, last_above_tol + 1) if modes is not None: selected_modes = min(selected_modes, modes) SVALS = np.sqrt(EVALS[:selected_modes]) EVECS = EVECS[:selected_modes] with logger.block( 'Computing left-singular vectors ({} vectors) ...'.format( len(EVECS))): POD = A.lincomb(EVECS / SVALS[:, np.newaxis]) if orthonormalize: with logger.block('Re-orthonormalizing POD modes ...'): POD = gram_schmidt(POD, product=product, copy=False) if check: logger.info('Checking orthonormality ...') 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 gram_schmidt_biorth(V, W, product=None, reiterate=True, reiteration_threshold=1e-1, check=True, check_tol=1e-3, copy=True): """Biorthonormalize a pair of |VectorArrays| using the biorthonormal Gram-Schmidt process. See Algorithm 1 in [BKS11]_. Parameters ---------- V, W The |VectorArrays| which are to be biorthonormalized. product The inner product |Operator| w.r.t. which to biorthonormalize. If `None`, the Euclidean product is used. reiterate If `True`, orthonormalize again if the norm of the orthogonalized vector is much smaller than the norm of the original vector. reiteration_threshold If `reiterate` is `True`, re-orthonormalize if the ratio between the norms of the orthogonalized vector and the original vector is smaller than this value. check If `True`, check if the resulting |VectorArray| is really orthonormal. check_tol Tolerance for the check. copy If `True`, create a copy of `V` and `W` instead of modifying `V` and `W` in-place. Returns ------- The biorthonormalized |VectorArrays|. """ assert V.space == W.space assert len(V) == len(W) logger = getLogger('pymor.algorithms.gram_schmidt.gram_schmidt_biorth') if copy: V = V.copy() W = W.copy() # main loop for i in range(len(V)): # calculate norm of V[i] initial_norm = V[i].norm(product)[0] # project V[i] if i == 0: V[0].scal(1 / initial_norm) else: norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during projection. while True: for j in range(i): # project by (I - V[j] * W[j]^T * E) p = W[j].pairwise_inner(V[i], product)[0] V[i].axpy(-p, V[j]) # calculate new norm old_norm, norm = norm, V[i].norm(product)[0] # check if reorthogonalization should be done if reiterate and norm < reiteration_threshold * old_norm: logger.info(f"Projecting vector V[{i}] again") else: V[i].scal(1 / norm) break # calculate norm of W[i] initial_norm = W[i].norm(product)[0] # project W[i] if i == 0: W[0].scal(1 / initial_norm) else: norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during projection. while True: for j in range(i): # project by (I - W[j] * V[j]^T * E) p = V[j].pairwise_inner(W[i], product)[0] W[i].axpy(-p, W[j]) # calculate new norm old_norm, norm = norm, W[i].norm(product)[0] # check if reorthogonalization should be done if reiterate and norm < reiteration_threshold * old_norm: logger.info(f"Projecting vector W[{i}] again") else: W[i].scal(1 / norm) break # rescale V[i] p = W[i].pairwise_inner(V[i], product)[0] V[i].scal(1 / p) if check: error_matrix = W.inner(V, product) error_matrix -= np.eye(len(V)) if error_matrix.size > 0: err = np.max(np.abs(error_matrix)) if err >= check_tol: raise AccuracyError(f"result not biorthogonal (max err={err})") return V, W
def gram_schmidt(A, product=None, return_R=False, atol=1e-13, rtol=1e-13, offset=0, reiterate=True, reiteration_threshold=9e-1, check=True, check_tol=1e-3, copy=True): """Orthonormalize a |VectorArray| using the modified Gram-Schmidt algorithm. Parameters ---------- A The |VectorArray| which is to be orthonormalized. product The inner product |Operator| w.r.t. which to orthonormalize. If `None`, the Euclidean product is used. return_R If `True`, the R matrix from QR decomposition is returned. atol Vectors of norm smaller than `atol` are removed from the array. rtol Relative tolerance used to detect linear dependent vectors (which are then removed from the array). offset Assume that the first `offset` vectors are already orthonormal and start the algorithm at the `offset + 1`-th vector. reiterate If `True`, orthonormalize again if the norm of the orthogonalized vector is much smaller than the norm of the original vector. reiteration_threshold If `reiterate` is `True`, re-orthonormalize if the ratio between the norms of the orthogonalized vector and the original vector is smaller than this value. check If `True`, check if the resulting |VectorArray| is really orthonormal. check_tol Tolerance for the check. copy If `True`, create a copy of `A` instead of modifying `A` in-place. Returns ------- Q The orthonormalized |VectorArray|. R The upper-triangular/trapezoidal matrix (if `compute_R` is `True`). """ logger = getLogger('pymor.algorithms.gram_schmidt.gram_schmidt') if copy: A = A.copy() # main loop R = np.eye(len(A)) remove = [] # indices of to be removed vectors for i in range(offset, len(A)): # first calculate norm initial_norm = A[i].norm(product)[0] if initial_norm < atol: logger.info(f"Removing vector {i} of norm {initial_norm}") remove.append(i) continue if i == 0: A[0].scal(1 / initial_norm) R[i, i] = initial_norm else: norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during orthogonalization (due to Andreas Buhr). while True: # orthogonalize to all vectors left for j in range(i): if j in remove: continue p = A[j].pairwise_inner(A[i], product)[0] A[i].axpy(-p, A[j]) common_dtype = np.promote_types(R.dtype, type(p)) R = R.astype(common_dtype, copy=False) R[j, i] += p # calculate new norm old_norm, norm = norm, A[i].norm(product)[0] # remove vector if it got too small if norm < rtol * initial_norm: logger.info(f"Removing linearly dependent vector {i}") remove.append(i) break # check if reorthogonalization should be done if reiterate and norm < reiteration_threshold * old_norm: logger.info(f"Orthonormalizing vector {i} again") else: A[i].scal(1 / norm) R[i, i] = norm break if remove: del A[remove] R = np.delete(R, remove, axis=0) if check: error_matrix = A[offset:len(A)].inner(A, product) error_matrix[:len(A) - offset, offset:len(A)] -= np.eye(len(A) - offset) if error_matrix.size > 0: err = np.max(np.abs(error_matrix)) if err >= check_tol: raise AccuracyError(f"result not orthogonal (max err={err})") if return_R: return A, R else: return A
def gram_schmidt(A, product=None, atol=1e-13, rtol=1e-13, offset=0, find_duplicates=True, reiterate=True, reiteration_threshold=1e-1, check=True, check_tol=1e-3, copy=True): """Orthonormalize a |VectorArray| using the stabilized Gram-Schmidt algorithm. Parameters ---------- A The |VectorArray| which is to be orthonormalized. product The scalar product w.r.t. which to orthonormalize, given as a linear |Operator|. If `None` the Euclidean product is used. atol Vectors of norm smaller than `atol` are removed from the array. rtol Relative tolerance used to detect linear dependent vectors (which are then removed from the array). offset Assume that the first `offset` vectors are already orthogonal and start the algorithm at the `offset + 1`-th vector. reiterate If `True`, orthonormalize again if the norm of the orthogonalized vector is much smaller than the norm of the original vector. reiteration_threshold If `reiterate` is `True`, re-orthonormalize if the ratio between the norms of the orthogonalized vector and the original vector is smaller than this value. check If `True`, check if the resulting VectorArray is really orthonormal. check_tol Tolerance for the check. copy If `True`, create a copy of `A` instead of modifying `A` itself. Returns ------- The orthonormalized |VectorArray|. """ logger = getLogger('pymor.algorithms.gram_schmidt.gram_schmidt') if copy: A = A.copy() # main loop remove = [] for i in range(offset, len(A)): # first calculate norm if product is None: initial_norm = A.l2_norm(ind=i)[0] else: initial_norm = np.sqrt( product.pairwise_apply2(A, A, V_ind=i, U_ind=i))[0] if initial_norm < atol: logger.info("Removing vector {} of norm {}".format( i, initial_norm)) remove.append(i) continue if i == 0: A.scal(1 / initial_norm, ind=0) else: first_iteration = True norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during orthonormalization (due to Andreas Buhr). while first_iteration or reiterate and norm / old_norm < reiteration_threshold: if first_iteration: first_iteration = False else: logger.info('Orthonormalizing vector {} again'.format(i)) # orthogonalize to all vectors left for j in range(i): if j in remove: continue if product is None: p = A.pairwise_dot(A, ind=i, o_ind=j)[0] else: p = product.pairwise_apply2(A, A, V_ind=i, U_ind=j)[0] A.axpy(-p, A, ind=i, x_ind=j) # calculate new norm if product is None: old_norm, norm = norm, A.l2_norm(ind=i)[0] else: old_norm, norm = norm, np.sqrt( product.pairwise_apply2(A, A, V_ind=i, U_ind=i))[0] # remove vector if it got too small: if norm / initial_norm < rtol: logger.info( "Removing linear dependent vector {}".format(i)) remove.append(i) break if norm > 0: A.scal(1 / norm, ind=i) if remove: A.remove(remove) if check: if product: error_matrix = product.apply2(A, A, V_ind=list(range(offset, len(A)))) else: error_matrix = A.dot(A, ind=list(range(offset, len(A)))) error_matrix[:len(A) - offset, offset:len(A)] -= np.eye(len(A) - offset) if error_matrix.size > 0: err = np.max(np.abs(error_matrix)) if err >= check_tol: raise AccuracyError( 'result not orthogonal (max err={})'.format(err)) return A
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:`la.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, pairwise=False) 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, pairwise=False), np.eye(len(POD)), atol=check_tol, rtol=0.): err = np.max(np.abs(POD.dot(POD, pairwise=False) - np.eye(len(POD)))) raise AccuracyError('result not orthogonal (max err={})'.format(err)) elif product and not float_cmp_all(product.apply2(POD, POD, pairwise=False), np.eye(len(POD)), atol=check_tol, rtol=0.): err = np.max(np.abs(product.apply2(POD, POD, pairwise=False) - 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 gram_schmidt_biorth(V, W, product=None, reiterate=True, reiteration_threshold=1e-1, check=True, check_tol=1e-3, copy=True): """Biorthonormalize a pair of |VectorArrays| using the biorthonormal Gram-Schmidt process. See Algorithm 1 in [BKS11]_. .. [BKS11] P. Benner, M. Köhler, J. Saak, Sparse-Dense Sylvester Equations in :math:`\mathcal{H}_2`-Model Order Reduction, Max Planck Institute Magdeburg Preprint, available from http://www.mpi-magdeburg.mpg.de/preprints/, 2011. Parameters ---------- V, W The |VectorArrays| which are to be biorthonormalized. product The inner product |Operator| w.r.t. which to biorthonormalize. If `None`, the Euclidean product is used. reiterate If `True`, orthonormalize again if the norm of the orthogonalized vector is much smaller than the norm of the original vector. reiteration_threshold If `reiterate` is `True`, re-orthonormalize if the ratio between the norms of the orthogonalized vector and the original vector is smaller than this value. check If `True`, check if the resulting |VectorArray| is really orthonormal. check_tol Tolerance for the check. copy If `True`, create a copy of `V` and `W` instead of modifying `V` and `W` in-place. Returns ------- The biorthonormalized |VectorArrays|. """ assert V.space == W.space assert len(V) == len(W) logger = getLogger('pymor.algorithms.gram_schmidt.gram_schmidt_biorth') if copy: V = V.copy() W = W.copy() # main loop for i in range(len(V)): # calculate norm of V[i] if product is None: initial_norm = V[i].l2_norm()[0] else: initial_norm = np.sqrt(product.pairwise_apply2(V[i], V[i]))[0] # project V[i] if i == 0: V[0].scal(1 / initial_norm) else: first_iteration = True norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during projection. while first_iteration or reiterate and norm / old_norm < reiteration_threshold: if first_iteration: first_iteration = False else: logger.info('Projecting vector V[{}] again'.format(i)) for j in range(i): # project by (I - V[j] * W[j]^T * E) if product is None: p = W[j].pairwise_dot(V[i])[0] else: p = product.pairwise_apply2(W[j], V[i])[0] V[i].axpy(-p, V[j]) # calculate new norm if product is None: old_norm, norm = norm, V[i].l2_norm()[0] else: old_norm, norm = norm, np.sqrt( product.pairwise_apply2(V[i], V[i])[0]) if norm > 0: V[i].scal(1 / norm) # calculate norm of W[i] if product is None: initial_norm = W[i].l2_norm()[0] else: initial_norm = np.sqrt(product.pairwise_apply2(W[i], W[i]))[0] # project W[i] if i == 0: W[0].scal(1 / initial_norm) else: first_iteration = True norm = initial_norm # If reiterate is True, reiterate as long as the norm of the vector changes # strongly during projection. while first_iteration or reiterate and norm / old_norm < reiteration_threshold: if first_iteration: first_iteration = False else: logger.info('Projecting vector W[{}] again'.format(i)) for j in range(i): # project by (I - W[j] * V[j]^T * E) if product is None: p = V[j].pairwise_dot(W[i])[0] else: p = product.pairwise_apply2(V[j], W[i])[0] W[i].axpy(-p, W[j]) # calculate new norm if product is None: old_norm, norm = norm, W[i].l2_norm()[0] else: old_norm, norm = norm, np.sqrt( product.pairwise_apply2(W[i], W[i])[0]) if norm > 0: W[i].scal(1 / norm) # rescale V[i] if product is None: p = W[i].pairwise_dot(V[i])[0] else: p = product.pairwise_apply2(W[i], V[i])[0] V[i].scal(1 / p) if check: if product: error_matrix = product.apply2(W, V) else: error_matrix = W.dot(V) error_matrix -= np.eye(len(V)) if error_matrix.size > 0: err = np.max(np.abs(error_matrix)) if err >= check_tol: raise AccuracyError( 'Result not biorthogonal (max err={})'.format(err)) return V, W