def test_gram_schmidt(): for i in (1, 32): b = NumpyVectorArray(np.identity(i, dtype=np.float)) a = gram_schmidt(b) assert b == a c = NumpyVectorArray([[1.0, 0], [0., 0]]) a = gram_schmidt(c) assert (a.data == np.array([[1.0, 0]])).all()
def test_gram_schmidt(): for i in (1, 32): b = NumpyVectorArray(np.identity(i, dtype=np.float)) a = gram_schmidt.gram_schmidt(b) assert b == a c = NumpyVectorArray([[1.0, 0], [0., 0]]) a = gram_schmidt.gram_schmidt(c) assert (a.data == np.array([[1.0, 0]])).all()
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 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 gram_schmidt_basis_extension(basis, U, U_ind=None, product=None, copy_basis=True, copy_U=True): '''Extend basis using Gram-Schmidt orthonormalization. Parameters ---------- basis The basis to extend. U The new basis vectors. U_ind Indices of the new basis vectors in U. product The scalar product w.r.t. which to orthonormalize; if None, the l2-scalar product on the coefficient vector 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 ------- The new basis. Raises ------ ExtensionError Gram-Schmidt orthonormalization fails. Usually this is the case when U is not linearily independent from the basis. However this can also happen due to rounding errors ... ''' if basis is None: basis = NumpyVectorArray(np.zeros((0, U.dim))) basis_length = len(basis) new_basis = basis.copy() if copy_basis else basis new_basis.append(U, o_ind=U_ind, remove_from_other=(not copy_U)) gram_schmidt(new_basis, offset=len(basis), product=product) if len(new_basis) <= basis_length: raise ExtensionError return new_basis
def pod(A, modes=None, product=None, tol=None, symmetrize=None, orthonormalize=False, check=None, check_tol=None): assert isinstance(A, VectorArrayInterface) assert len(A) > 0 assert modes is None or modes <= len(A) assert product is None or isinstance(product, OperatorInterface) tol = defaults.pod_tol if tol is None else tol symmetrize = defaults.pod_symmetrize if symmetrize is None else symmetrize orthonormalize = defaults.pod_orthonormalize if orthonormalize is None else orthonormalize check = defaults.pod_check if check is None else check check_tol = defaults.pod_check_tol if check_tol is None else check_tol 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? last_above_tol = np.where(EVALS >= tol)[0][-1] EVALS = EVALS[:last_above_tol + 1] EVECS = EVECS[:last_above_tol + 1] if len(EVALS) == 0: return type(A).empty(A.dim) POD = A.lincomb(EVECS / np.sqrt(EVALS[:, np.newaxis])) if orthonormalize: POD = gram_schmidt(POD, product=product) if check: if not product and not float_cmp_all(POD.dot(POD, pairwise=False), np.eye(len(POD)), check_tol): 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)), check_tol): err = np.max(np.abs(product.apply2(POD, POD, pairwise=False) - np.eye(len(POD)))) raise AccuracyError('result not orthogonal (max err={})'.format(err)) return POD
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(source_basis=RB, range_basis=residual_range) 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], NonProjectedResiudalOperator): 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, ind, residual_range): if isinstance(op, LincombOperator): for o in op.operators: collect_operator_ranges(o, ind, residual_range) elif isinstance(op, EmpiricalInterpolatedOperator): if hasattr(op, 'collateral_basis') and ind == -1: residual_range.append(op.collateral_basis) elif op.linear and not op.parametric: if ind >= 0: residual_range.append(op.apply(RB, ind=ind)) else: raise CollectionError(op) def collect_functional_ranges(op, residual_range): if isinstance(op, LincombOperator): for o in op.operators: collect_functional_ranges(o, 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, i, new_residual_range) except CollectionError as e: logger.warn('Cannot compute range of {}. Evaluation will be slow.'.format(e.op)) operator = operator.projected(RB, None) return (NonProjectedResiudalOperator(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(RB, residual_range, product=None) # the product always cancels out. functional = functional.projected(residual_range, None, product=None) return (ResidualOperator(operator, functional), GenericRBReconstructor(residual_range), {'residual_range_dims': residual_range_dims})
def pod(A, modes=None, product=None, tol=None, symmetrize=None, orthonormalize=None, check=None, check_tol=None): '''Proper orthogonal decomposition of `A`. If the |VectorArray| `A` is viewed as a map :: A: R^(len(A)) ---> R^(dim(A)) then the return value of this method is simply the array of left-singular vectors of the singular value decomposition of `A`, where the scalar product on R^(dim(A) is given by `product` and the scalar product on R^(len(A)) is 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 given as a linear |Operator| w.r.t. compute the POD. tol Squared singular values smaller than this value are ignored. If `None`, the `pod_tol` |default| value is used. symmetrize If `True` symmetrize the gramian again before proceeding. If `None`, the `pod_symmetrize` |default| value is chosen. orthonormalize If `True`, orthonormalize the computed POD modes again using :func:`la.gram_schmidt.gram_schmidt`. If `None`, the `pod_orthonormalize` |default| value is used. check If `True`, check the computed POD modes for orthonormality. If `None`, the `pod_check` |default| value is used. check_tol Tolerance for the orthonormality check. If `None`, the `pod_check_tol` |default| value is used. Returns ------- |VectorArray| of POD modes. ''' assert isinstance(A, VectorArrayInterface) assert len(A) > 0 assert modes is None or modes <= len(A) assert product is None or isinstance(product, OperatorInterface) tol = defaults.pod_tol if tol is None else tol symmetrize = defaults.pod_symmetrize if symmetrize is None else symmetrize orthonormalize = defaults.pod_orthonormalize if orthonormalize is None else orthonormalize check = defaults.pod_check if check is None else check check_tol = defaults.pod_check_tol if check_tol is None else check_tol 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)[0] if len(above_tol) == 0: return type(A).empty(A.dim) last_above_tol = above_tol[-1] EVALS = EVALS[:last_above_tol + 1] EVECS = EVECS[:last_above_tol + 1] POD = A.lincomb(EVECS / np.sqrt(EVALS[:, 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)), check_tol): 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)), check_tol): err = np.max(np.abs(product.apply2(POD, POD, pairwise=False) - np.eye(len(POD)))) raise AccuracyError('result not orthogonal (max err={})'.format(err)) return POD
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, pairwise=False)) else: U_proj_err = U - basis.lincomb(product.apply2(U, basis, pairwise=False)) 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 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 reduce_residual(operator, functional=None, RB=None, product=None, extends=None): """Generic reduced basis residual reductor. """ assert functional == 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], NonProjectedResiudalOperator): 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, ind, residual_range): if isinstance(op, LincombOperator): for o in op.operators: collect_operator_ranges(o, ind, residual_range) elif isinstance(op, EmpiricalInterpolatedOperator): if hasattr(op, 'collateral_basis') and ind == -1: residual_range.append(op.collateral_basis) elif op.linear and not op.parametric: if ind >= 0: residual_range.append(op.apply(RB, ind=ind)) else: raise CollectionError(op) def collect_functional_ranges(op, residual_range): if isinstance(op, LincombOperator): for o in op.operators: collect_functional_ranges(o, 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, i, new_residual_range) except CollectionError as e: logger.warn('Cannot compute range of {}. Evaluation will be slow.'.format(e.op)) operator = operator.projected(RB, None) return (NonProjectedResiudalOperator(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(RB, residual_range, product=None) # the product always cancels out. functional = functional.projected(residual_range, None, product=None) return (ResidualOperator(operator, functional), GenericRBReconstructor(residual_range), {'residual_range_dims': residual_range_dims})
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, pairwise=False)) else: U_proj_err = U - basis.lincomb(product.apply2(U, basis, pairwise=False)) 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}