Source code for pymor.reductors.residual

# This file is part of the pyMOR project (http://www.pymor.org).
# Copyright 2013-2020 pyMOR developers and contributors. All rights reserved.
# License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)

import numpy as np

from pymor.algorithms.image import estimate_image_hierarchical
from pymor.algorithms.projection import project, project_to_subbasis
from pymor.core.base import BasicObject
from pymor.core.exceptions import ImageCollectionError
from pymor.operators.constructions import induced_norm
from pymor.operators.interface import Operator


[docs]class ResidualReductor(BasicObject): """Generic reduced basis residual reductor. Given an operator and a right-hand side, the residual is given by:: residual.apply(U, mu) == operator.apply(U, mu) - rhs.as_range_array(mu) When operator maps to functionals instead of vectors, we are interested in the Riesz representative of the residual:: residual.apply(U, mu) == product.apply_inverse(operator.apply(U, mu) - rhs.as_range_array(mu)) Given a basis `RB` of a subspace of the source space of `operator`, this reductor uses :func:`~pymor.algorithms.image.estimate_image_hierarchical` to determine a low-dimensional subspace containing the image of the subspace under `residual` (resp. `riesz_residual`), computes an orthonormal basis `residual_range` for this range space and then returns the Petrov-Galerkin projection :: projected_residual == project(residual, range_basis=residual_range, source_basis=RB) of the residual operator. Given a reduced basis coefficient vector `u`, w.r.t. `RB`, the (dual) norm of the residual can then be computed as :: projected_residual.apply(u, mu).l2_norm() Moreover, a `reconstruct` method is provided such that :: residual_reductor.reconstruct(projected_residual.apply(u, mu)) == residual.apply(RB.lincomb(u), mu) Parameters ---------- RB |VectorArray| containing a basis of the reduced space onto which to project. operator See definition of `residual`. rhs See definition of `residual`. If `None`, zero right-hand side is assumed. product Inner product |Operator| w.r.t. which to orthonormalize and w.r.t. which to compute the Riesz representatives in case `operator` maps to functionals. riesz_representatives If `True` compute the Riesz representative of the residual. """ def __init__(self, RB, operator, rhs=None, product=None, riesz_representatives=False): assert RB in operator.source assert rhs is None \ or (rhs.source.is_scalar and rhs.range == operator.range and rhs.linear) assert product is None or product.source == product.range == operator.range self.__auto_init(locals()) self.residual_range = operator.range.empty() self.residual_range_dims = [] def reduce(self): if self.residual_range is not False: with self.logger.block('Estimating residual range ...'): try: self.residual_range, self.residual_range_dims = \ estimate_image_hierarchical([self.operator], [self.rhs], self.RB, (self.residual_range, self.residual_range_dims), orthonormalize=True, product=self.product, riesz_representatives=self.riesz_representatives) except ImageCollectionError as e: self.logger.warning(f'Cannot compute range of {e.op}. Evaluation will be slow.') self.residual_range = False if self.residual_range is False: operator = project(self.operator, None, self.RB) return NonProjectedResidualOperator(operator, self.rhs, self.riesz_representatives, self.product) with self.logger.block('Projecting residual operator ...'): if self.riesz_representatives: operator = project(self.operator, self.residual_range, self.RB, product=None) # the product cancels out. rhs = project(self.rhs, self.residual_range, None, product=None) else: operator = project(self.operator, self.residual_range, self.RB, product=self.product) rhs = project(self.rhs, self.residual_range, None, product=self.product) return ResidualOperator(operator, rhs)
[docs] def reconstruct(self, u): """Reconstruct high-dimensional residual vector from reduced vector `u`.""" if self.residual_range is False: if self.product: norm = induced_norm(self.product) return u * (u.l2_norm() / norm(u))[0] else: return u else: return self.residual_range[:u.dim].lincomb(u.to_numpy())
[docs]class ResidualOperator(Operator): """Instantiated by :class:`ResidualReductor`.""" def __init__(self, operator, rhs, name=None): self.__auto_init(locals()) self.source = operator.source self.range = operator.range self.linear = operator.linear self.rhs_vector = rhs.as_range_array() if rhs and not rhs.parametric else None
[docs] def apply(self, U, mu=None): V = self.operator.apply(U, mu=mu) if self.rhs: F = self.rhs_vector or self.rhs.as_range_array(mu) if len(V) > 1: V -= F[[0]*len(V)] else: V -= F return V
def projected_to_subbasis(self, dim_range=None, dim_source=None, name=None): return ResidualOperator(project_to_subbasis(self.operator, dim_range, dim_source), project_to_subbasis(self.rhs, dim_range, None), name=name)
[docs]class NonProjectedResidualOperator(ResidualOperator): """Instantiated by :class:`ResidualReductor`. Not to be used directly. """ def __init__(self, operator, rhs, riesz_representatives, product): super().__init__(operator, rhs) self.__auto_init(locals())
[docs] def apply(self, U, mu=None): R = super().apply(U, mu=mu) if self.product: if self.riesz_representatives: R_riesz = self.product.apply_inverse(R) # divide by norm, except when norm is zero: inversel2 = 1./R_riesz.l2_norm() inversel2 = np.nan_to_num(inversel2) R_riesz.scal(np.sqrt(R_riesz.pairwise_dot(R)) * inversel2) return R_riesz else: # divide by norm, except when norm is zero: inversel2 = 1./R.l2_norm() inversel2 = np.nan_to_num(inversel2) R.scal(np.sqrt(self.product.pairwise_apply2(R, R)) * inversel2) return R else: return R
def projected_to_subbasis(self, dim_range=None, dim_source=None, name=None): return self.with_(operator=project_to_subbasis(self.operator, None, dim_source))
[docs]class ImplicitEulerResidualReductor(BasicObject): """Reduced basis residual reductor with mass operator for implicit Euler timestepping. Given an operator, mass and a functional, the concatenation of residual operator with the Riesz isomorphism is given by:: riesz_residual.apply(U, U_old, mu) == product.apply_inverse(operator.apply(U, mu) + 1/dt*mass.apply(U, mu) - 1/dt*mass.apply(U_old, mu) - rhs.as_vector(mu)) This reductor determines a low-dimensional subspace of the image of a reduced basis space under `riesz_residual` using :func:`~pymor.algorithms.image.estimate_image_hierarchical`, computes an orthonormal basis `residual_range` of this range space 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 reduced basis coefficient vectors `u` and `u_old`, the dual norm of the residual can then be computed as :: projected_riesz_residual.apply(u, u_old, mu).l2_norm() Moreover, a `reconstruct` method is provided such that :: residual_reductor.reconstruct(projected_riesz_residual.apply(u, u_old, mu)) == riesz_residual.apply(RB.lincomb(u), RB.lincomb(u_old), mu) Parameters ---------- operator See definition of `riesz_residual`. mass The mass operator. See definition of `riesz_residual`. dt The time step size. See definition of `riesz_residual`. rhs 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 Inner product |Operator| w.r.t. which to compute the Riesz representatives. """ def __init__(self, RB, operator, mass, dt, rhs=None, product=None): assert RB in operator.source assert rhs is None \ or rhs.source.is_scalar and rhs.range == operator.range and rhs.linear assert product is None or product.source == product.range == operator.range self.__auto_init(locals()) self.residual_range = operator.range.empty() self.residual_range_dims = [] def reduce(self): if self.residual_range is not False: with self.logger.block('Estimating residual range ...'): try: self.residual_range, self.residual_range_dims = \ estimate_image_hierarchical([self.operator, self.mass], [self.rhs], self.RB, (self.residual_range, self.residual_range_dims), orthonormalize=True, product=self.product, riesz_representatives=True) except ImageCollectionError as e: self.logger.warning(f'Cannot compute range of {e.op}. Evaluation will be slow.') self.residual_range = False if self.residual_range is False: operator = project(self.operator, None, self.RB) mass = project(self.mass, None, self.RB) return NonProjectedImplicitEulerResidualOperator(operator, mass, self.rhs, self.dt, self.product) with self.logger.block('Projecting residual operator ...'): operator = project(self.operator, self.residual_range, self.RB, product=None) # the product always cancels out. mass = project(self.mass, self.residual_range, self.RB, product=None) rhs = project(self.rhs, self.residual_range, None, product=None) return ImplicitEulerResidualOperator(operator, mass, rhs, self.dt)
[docs] def reconstruct(self, u): """Reconstruct high-dimensional residual vector from reduced vector `u`.""" if self.residual_range is False: if self.product: norm = induced_norm(self.product) return u * (u.l2_norm() / norm(u))[0] else: return u else: return self.residual_range[:u.dim].lincomb(u.to_numpy())
[docs]class ImplicitEulerResidualOperator(Operator): """Instantiated by :class:`ImplicitEulerResidualReductor`.""" def __init__(self, operator, mass, rhs, dt, name=None): self.__auto_init(locals()) self.source = operator.source self.range = operator.range self.linear = operator.linear self.rhs_vector = rhs.as_range_array() if rhs and not rhs.parametric else None
[docs] def apply(self, U, U_old, mu=None): V = self.operator.apply(U, mu=mu) V.axpy(1./self.dt, self.mass.apply(U, mu=mu)) V.axpy(-1./self.dt, self.mass.apply(U_old, mu=mu)) if self.rhs: F = self.rhs_vector or self.rhs.as_range_array(mu) if len(V) > 1: V -= F[[0]*len(V)] else: V -= F return V
def projected_to_subbasis(self, dim_range=None, dim_source=None, name=None): return ImplicitEulerResidualOperator(project_to_subbasis(self.operator, dim_range, dim_source), project_to_subbasis(self.mass, dim_range, dim_source), project_to_subbasis(self.rhs, dim_range, None), self.dt, name=name)
[docs]class NonProjectedImplicitEulerResidualOperator(ImplicitEulerResidualOperator): """Instantiated by :class:`ImplicitEulerResidualReductor`. Not to be used directly. """ def __init__(self, operator, mass, rhs, dt, product): super().__init__(operator, mass, rhs, dt) self.product = product
[docs] def apply(self, U, U_old, mu=None): R = super().apply(U, U_old, mu=mu) if self.product: R_riesz = self.product.apply_inverse(R) # divide by norm, except when norm is zero: inversel2 = 1./R_riesz.l2_norm() inversel2 = np.nan_to_num(inversel2) R_riesz.scal(np.sqrt(R_riesz.pairwise_dot(R)) * inversel2) return R_riesz else: return R
def projected_to_subbasis(self, dim_range=None, dim_source=None, name=None): return self.with_(operator=project_to_subbasis(self.operator, None, dim_source), mass=project_to_subbasis(self.mass, None, dim_source))