Exemplo n.º 1
0
    def __init__(self,
                 operators,
                 functionals,
                 vector_operators,
                 products=None,
                 estimator=None,
                 visualizer=None,
                 cache_region='disk',
                 name=None):
        self.operators = FrozenDict(operators)
        self.functionals = FrozenDict(functionals)
        self.vector_operators = FrozenDict(vector_operators)
        self.linear = all(
            op is None or op.linear
            for op in chain(operators.itervalues(), functionals.itervalues()))
        self.products = products
        self.estimator = estimator
        self.visualizer = visualizer
        self.cache_region = cache_region
        self.name = name

        if products:
            for k, v in products.iteritems():
                setattr(self, '{}_product'.format(k), v)
                setattr(self, '{}_norm'.format(k), induced_norm(v))
Exemplo n.º 2
0
    def __init__(self,
                 operator,
                 rhs,
                 outputs=None,
                 products=None,
                 parameter_space=None,
                 estimator=None,
                 visualizer=None,
                 cache_region=None,
                 name=None):

        if isinstance(rhs, VectorArrayInterface):
            assert rhs in operator.range
            rhs = VectorOperator(rhs, name='rhs')

        assert rhs.range == operator.range and rhs.source.is_scalar and rhs.linear

        super().__init__(products=products,
                         estimator=estimator,
                         visualizer=visualizer,
                         cache_region=cache_region,
                         name=name)
        self.operator = operator
        self.rhs = rhs
        self.outputs = FrozenDict(outputs or {})
        self.solution_space = self.operator.source
        self.build_parameter_type(operator, rhs)
        self.parameter_space = parameter_space
Exemplo n.º 3
0
    def __init__(self,
                 stationary_part,
                 initial_data,
                 T=1.,
                 parameter_ranges=None,
                 name=None):
        name = name or ('instationary_' + stationary_part.name)
        assert (initial_data is None
                or initial_data.dim_domain == stationary_part.domain.dim
                and initial_data.shape_range == ())
        assert (
            parameter_ranges is None
            or (isinstance(parameter_ranges,
                           (list, tuple)) and len(parameter_ranges) == 2
                and parameter_ranges[0] <= parameter_ranges[1])
            or (isinstance(parameter_ranges, dict) and all(
                isinstance(v, (list, tuple)) and len(v) == 2 and v[0] <= v[1]
                for v in parameter_ranges.values())))

        parameter_ranges = (None if parameter_ranges is None else
                            tuple(parameter_ranges) if isinstance(
                                parameter_ranges,
                                (list, tuple)) else FrozenDict(
                                    (k, tuple(v))
                                    for k, v in parameter_ranges.items()))
        self.__auto_init(locals())
Exemplo n.º 4
0
    def __init__(self,
                 operators=None,
                 products=None,
                 estimator=None,
                 visualizer=None,
                 cache_region=None,
                 name=None,
                 **kwargs):

        operators = {} if operators is None else dict(operators)

        # handle special operators
        for on in self.special_operators:
            # special operators must be provided as keyword argument to __init__
            assert on in kwargs
            # special operators may not already exist as attributes
            assert not hasattr(self, on)
            # special operators may not be contained in operators dict
            assert on not in operators

            op = kwargs[on]
            # operators either have to be None or an Operator
            assert op is None \
                or isinstance(op, OperatorInterface) \
                or all(isinstance(o, OperatorInterface) for o in op)
            # set special operator as an attribute
            setattr(self, on, op)
            # add special operator to the operators dict
            operators[on] = op

        self.operators = FrozenDict(operators)
        self.linear = all(op is None or op.linear for op in operators.values())
        self.products = FrozenDict(products or {})
        self.estimator = estimator
        self.visualizer = visualizer
        self.enable_caching(cache_region)
        self.name = name

        if products:
            for k, v in products.items():
                setattr(self, '{}_product'.format(k), v)
                setattr(self, '{}_norm'.format(k), induced_norm(v))

        self.build_parameter_type(*operators.values())
Exemplo n.º 5
0
    def __init__(self, products=None, estimator=None, visualizer=None,
                 name=None, **kwargs):

        products = FrozenDict(products or {})

        if products:
            for k, v in products.items():
                setattr(self, f'{k}_product', v)
                setattr(self, f'{k}_norm', induced_norm(v))

        self.__auto_init(locals())
Exemplo n.º 6
0
    def __init__(self,
                 T,
                 initial_data,
                 operator,
                 rhs,
                 mass=None,
                 time_stepper=None,
                 num_values=None,
                 outputs=None,
                 products=None,
                 parameter_space=None,
                 estimator=None,
                 visualizer=None,
                 cache_region=None,
                 name=None):

        if isinstance(rhs, VectorArrayInterface):
            assert rhs in operator.range
            rhs = VectorOperator(rhs, name='rhs')
        if isinstance(initial_data, VectorArrayInterface):
            assert initial_data in operator.source
            initial_data = VectorOperator(initial_data, name='initial_data')

        assert isinstance(time_stepper, TimeStepperInterface)
        assert initial_data.source.is_scalar
        assert operator.source == initial_data.range
        assert rhs is None \
            or rhs.linear and rhs.range == operator.range and rhs.source.is_scalar
        assert mass is None \
            or mass.linear and mass.source == mass.range == operator.source

        super().__init__(products=products,
                         estimator=estimator,
                         visualizer=visualizer,
                         cache_region=cache_region,
                         name=name)
        self.T = T
        self.initial_data = initial_data
        self.operator = operator
        self.rhs = rhs
        self.mass = mass
        self.solution_space = self.operator.source
        self.time_stepper = time_stepper
        self.num_values = num_values
        self.outputs = FrozenDict(outputs or {})
        self.linear = operator.linear and all(
            output.linear for output in self.outputs.values())
        self.build_parameter_type(self.initial_data,
                                  self.operator,
                                  self.rhs,
                                  self.mass,
                                  provides={'_t': 0})
        self.parameter_space = parameter_space
Exemplo n.º 7
0
 def __init__(self, parameters, *ranges):
     assert isinstance(parameters, Parameters)
     assert 1 <= len(ranges) <= 2
     if len(ranges) == 1:
         ranges = ranges[0]
     if isinstance(ranges, (tuple, list)):
         assert len(ranges) == 2
         ranges = {k: ranges for k in parameters}
     assert isinstance(ranges, dict)
     assert all(k in ranges and len(ranges[k]) == 2 and all(
         isinstance(v, Number)
         for v in ranges[k]) and ranges[k][0] <= ranges[k][1]
                for k in parameters)
     self.parameters = parameters
     self.ranges = FrozenDict((k, tuple(v)) for k, v in ranges.items())
Exemplo n.º 8
0
    def __init__(self,
                 products=None,
                 estimator=None,
                 visualizer=None,
                 cache_region=None,
                 name=None,
                 **kwargs):

        self.products = FrozenDict(products or {})
        self.estimator = estimator
        self.visualizer = visualizer
        self.enable_caching(cache_region)
        self.name = name

        if products:
            for k, v in products.items():
                setattr(self, f'{k}_product', v)
                setattr(self, f'{k}_norm', induced_norm(v))
Exemplo n.º 9
0
    def __init__(self,
                 domain,
                 rhs=None,
                 diffusion=None,
                 advection=None,
                 nonlinear_advection=None,
                 nonlinear_advection_derivative=None,
                 reaction=None,
                 nonlinear_reaction=None,
                 nonlinear_reaction_derivative=None,
                 dirichlet_data=None,
                 neumann_data=None,
                 robin_data=None,
                 functionals=None,
                 parameter_space=None,
                 name=None):

        assert (rhs is None
                or rhs.dim_domain == domain.dim and rhs.shape_range == ())
        assert (diffusion is None or diffusion.dim_domain == domain.dim
                and diffusion.shape_range in ((), (domain.dim, domain.dim)))
        assert (advection is None or advection.dim_domain == domain.dim
                and advection.shape_range == (domain.dim, ))
        assert (nonlinear_advection is None
                or nonlinear_advection.dim_domain == 1
                and nonlinear_advection.shape_range == (domain.dim, ))
        assert (nonlinear_advection_derivative is None or
                (nonlinear_advection_derivative.dim_domain == 1 and
                 nonlinear_advection_derivative.shape_range == (domain.dim, )))
        assert (reaction is None or reaction.dim_domain == domain.dim
                and reaction.shape_range == ())
        assert (nonlinear_reaction is None
                or nonlinear_reaction.dim_domain == 1
                and nonlinear_reaction.shape_range == ())
        assert (nonlinear_reaction_derivative is None
                or nonlinear_reaction_derivative.dim_domain == 1
                and nonlinear_reaction_derivative.shape_range == ())
        assert (dirichlet_data is None
                or dirichlet_data.dim_domain == domain.dim
                and dirichlet_data.shape_range == ())
        assert (neumann_data is None or neumann_data.dim_domain == domain.dim
                and neumann_data.shape_range == ())
        assert (robin_data is None
                or (isinstance(robin_data, tuple) and len(robin_data) == 2
                    and np.all([
                        f.dim_domain == domain.dim and f.shape_range == ()
                        for f in robin_data
                    ])))
        assert (functionals is None or all(
            isinstance(v, tuple) and len(v) == 2 and v[0] in ('l2',
                                                              'l2_boundary')
            and v[1].dim_domain == domain.dim and v[1].shape_range == ()
            for v in functionals.values()))

        self.domain = domain
        self.rhs = rhs
        self.diffusion = diffusion
        self.advection = advection
        self.nonlinear_advection = nonlinear_advection
        self.nonlinear_advection_derivative = nonlinear_advection_derivative
        self.reaction = reaction
        self.nonlinear_reaction = nonlinear_reaction
        self.nonlinear_reaction_derivative = nonlinear_reaction_derivative
        self.dirichlet_data = dirichlet_data
        self.neumann_data = neumann_data
        self.robin_data = robin_data
        self.functionals = FrozenDict(
            functionals) if functionals is not None else None
        self.parameter_space = parameter_space
        self.name = name
Exemplo n.º 10
0
class Model(CacheableObject, Parametric):
    """Interface for model objects.

    A model object defines a discrete problem
    via its `class` and the |Operators| it contains.
    Furthermore, models can be
    :meth:`solved <Model.solve>` for a given
    |Parameter| resulting in a solution |VectorArray|.

    Attributes
    ----------
    solution_space
        |VectorSpace| of the solution |VectorArrays| returned by :meth:`solve`.
    output_space
        |VectorSpace| of the model output |VectorArrays| returned by
        :meth:`output` (typically `NumpyVectorSpace(k)` where `k` is a small).
    linear
        `True` if the model describes a linear problem.
    products
        Dict of inner product operators associated with the model.
    """

    solution_space = None
    output_space = None
    linear = False
    products = FrozenDict()

    def __init__(self,
                 products=None,
                 estimator=None,
                 visualizer=None,
                 name=None,
                 **kwargs):
        products = FrozenDict(products or {})
        if products:
            for k, v in products.items():
                setattr(self, f'{k}_product', v)
                setattr(self, f'{k}_norm', induced_norm(v))

        self.__auto_init(locals())

    @abstractmethod
    def _solve(self, mu=None, return_output=False, **kwargs):
        """Perform the actual solving."""
        pass

    def solve(self, mu=None, return_output=False, **kwargs):
        """Solve the discrete problem for the |Parameter| `mu`.

        The result will be :mod:`cached <pymor.core.cache>`
        in case caching has been activated for the given model.

        Parameters
        ----------
        mu
            |Parameter| for which to solve.
        return_output
            If `True`, the model output for the given |Parameter| `mu` is
            returned as a |VectorArray| from :attr:`output_space`.

        Returns
        -------
        The solution |VectorArray|. When `return_output` is `True`,
        the output |VectorArray| is returned as second value.
        """
        mu = self.parse_parameter(mu)
        return self.cached_method_call(self._solve,
                                       mu=mu,
                                       return_output=return_output,
                                       **kwargs)

    def output(self, mu=None, **kwargs):
        """Return the model output for given |Parameter| `mu`.

        Parameters
        ----------
        mu
            |Parameter| for which to compute the output.

        Returns
        -------
        The computed model output as a |VectorArray| from `output_space`.
        """
        return self.solve(mu=mu, return_output=True, **kwargs)[1]

    def estimate(self, U, mu=None):
        """Estimate the model error for a given solution.

        The model error could be the error w.r.t. the analytical
        solution of the given problem or the model reduction error w.r.t.
        a corresponding high-dimensional |Model|.

        Parameters
        ----------
        U
            The solution obtained by :meth:`~solve`.
        mu
            |Parameter| for which `U` has been obtained.

        Returns
        -------
        The estimated error.
        """
        if getattr(self, 'estimator') is not None:
            return self.estimator.estimate(U, mu=mu, m=self)
        else:
            raise NotImplementedError('Model has no estimator.')

    def visualize(self, U, **kwargs):
        """Visualize a solution |VectorArray| U.

        Parameters
        ----------
        U
            The |VectorArray| from
            :attr:`~pymor.models.interface.Model.solution_space`
            that shall be visualized.
        kwargs
            See docstring of `self.visualizer.visualize`.
        """
        if getattr(self, 'visualizer') is not None:
            return self.visualizer.visualize(U, self, **kwargs)
        else:
            raise NotImplementedError('Model has no visualizer.')
Exemplo n.º 11
0
    def __init__(self,
                 domain,
                 rhs=None,
                 diffusion=None,
                 advection=None,
                 nonlinear_advection=None,
                 nonlinear_advection_derivative=None,
                 reaction=None,
                 nonlinear_reaction=None,
                 nonlinear_reaction_derivative=None,
                 dirichlet_data=None,
                 neumann_data=None,
                 robin_data=None,
                 outputs=None,
                 parameter_ranges=None,
                 name=None):

        assert (rhs is None
                or rhs.dim_domain == domain.dim and rhs.shape_range == ())
        assert (diffusion is None or diffusion.dim_domain == domain.dim
                and diffusion.shape_range in ((), (domain.dim, domain.dim)))
        assert (advection is None or advection.dim_domain == domain.dim
                and advection.shape_range == (domain.dim, ))
        assert (nonlinear_advection is None
                or nonlinear_advection.dim_domain == 1
                and nonlinear_advection.shape_range == (domain.dim, ))
        assert (nonlinear_advection_derivative is None or
                (nonlinear_advection_derivative.dim_domain == 1 and
                 nonlinear_advection_derivative.shape_range == (domain.dim, )))
        assert (reaction is None or reaction.dim_domain == domain.dim
                and reaction.shape_range == ())
        assert (nonlinear_reaction is None
                or nonlinear_reaction.dim_domain == 1
                and nonlinear_reaction.shape_range == ())
        assert (nonlinear_reaction_derivative is None
                or nonlinear_reaction_derivative.dim_domain == 1
                and nonlinear_reaction_derivative.shape_range == ())
        assert (dirichlet_data is None
                or dirichlet_data.dim_domain == domain.dim
                and dirichlet_data.shape_range == ())
        assert (neumann_data is None or neumann_data.dim_domain == domain.dim
                and neumann_data.shape_range == ())
        assert (robin_data is None
                or (isinstance(robin_data, tuple) and len(robin_data) == 2
                    and np.all([
                        f.dim_domain == domain.dim and f.shape_range == ()
                        for f in robin_data
                    ])))
        assert (outputs is None or all(
            isinstance(v, tuple) and len(v) == 2 and v[0] in ('l2',
                                                              'l2_boundary')
            and v[1].dim_domain == domain.dim and v[1].shape_range == ()
            for v in outputs))
        assert (
            parameter_ranges is None
            or (isinstance(parameter_ranges,
                           (list, tuple)) and len(parameter_ranges) == 2
                and parameter_ranges[0] <= parameter_ranges[1])
            or (isinstance(parameter_ranges, dict) and all(
                isinstance(v, (list, tuple)) and len(v) == 2 and v[0] <= v[1]
                for v in parameter_ranges.values())))

        outputs = tuple(outputs) if outputs is not None else None
        parameter_ranges = (None if parameter_ranges is None else
                            tuple(parameter_ranges) if isinstance(
                                parameter_ranges,
                                (list, tuple)) else FrozenDict(
                                    (k, tuple(v))
                                    for k, v in parameter_ranges.items()))

        self.__auto_init(locals())
Exemplo n.º 12
0
class Model(CacheableObject, ParametricObject):
    """Interface for model objects.

    A model object defines a discrete problem
    via its `class` and the |Operators| it contains.
    Furthermore, models can be
    :meth:`solved <Model.solve>` for given
    |parameter values| resulting in a solution |VectorArray|.

    Attributes
    ----------
    solution_space
        |VectorSpace| of the solution |VectorArrays| returned by :meth:`solve`.
    dim_output
        Dimension of the model output returned by :meth:`output`. 0 if the
        model has no output.
    linear
        `True` if the model describes a linear problem.
    products
        Dict of inner product operators associated with the model.
    """

    solution_space = None
    dim_output = 0
    linear = False
    products = FrozenDict()

    def __init__(self,
                 products=None,
                 error_estimator=None,
                 visualizer=None,
                 name=None):
        products = FrozenDict(products or {})
        if products:
            for k, v in products.items():
                setattr(self, f'{k}_product', v)
                setattr(self, f'{k}_norm', induced_norm(v))

        self.__auto_init(locals())

    def _compute(self,
                 solution=False,
                 output=False,
                 solution_d_mu=False,
                 output_d_mu=False,
                 solution_error_estimate=False,
                 output_error_estimate=False,
                 output_d_mu_return_array=False,
                 mu=None,
                 **kwargs):
        return {}

    def _compute_solution(self, mu=None, **kwargs):
        """Compute the model's solution for |parameter values| `mu`.

        This method is called by the default implementation of :meth:`compute`
        in :class:`pymor.models.interface.Model`.

        Parameters
        ----------
        mu
            |Parameter values| for which to compute the solution.
        kwargs
            Additional keyword arguments to customize how the solution is
            computed or to select additional data to be returned.

        Returns
        -------
        |VectorArray| with the computed solution or a dict which at least
        must contain the key `'solution'`.
        """
        raise NotImplementedError

    def _compute_output(self, solution, mu=None, **kwargs):
        """Compute the model's output for |parameter values| `mu`.

        This method is called by the default implementation of :meth:`compute`
        in :class:`pymor.models.interface.Model`. The assumption is made
        that the output is a derived quantity from the model's internal state
        as returned my :meth:`_compute_solution`. When this is not the case,
        the computation of the output should be implemented in :meth:`_compute`.

        .. note::

            The default implementation applies the |Operator| given by the
            :attr:`output_functional` attribute to the given `solution`
            |VectorArray|.

        Parameters
        ----------
        solution
            Internal model state for the given |parameter values|.
        mu
            |Parameter values| for which to compute the output.
        kwargs
            Additional keyword arguments to customize how the output is
            computed or to select additional data to be returned.

        Returns
        -------
        |NumPy array| with the computed output or a dict which at least
        must contain the key `'output'`.
        """
        if not getattr(self, 'output_functional', None):
            return np.zeros(len(solution), 0)
        else:
            return self.output_functional.apply(solution, mu=mu).to_numpy()

    def _compute_solution_d_mu_single_direction(self,
                                                parameter,
                                                index,
                                                solution,
                                                mu=None,
                                                **kwargs):
        """Compute the partial derivative of the solution w.r.t. a parameter index

        Parameters
        ----------
        parameter
            parameter for which to compute the sensitivity
        index
            parameter index for which to compute the sensitivity
        solution
            Internal model state for the given |Parameter value|.
        mu
            |Parameter value| for which to solve

        Returns
        -------
        The sensitivity of the solution as a |VectorArray|.
        """
        raise NotImplementedError

    def _compute_solution_d_mu(self, solution, mu=None, **kwargs):
        """Compute all partial derivative of the solution w.r.t. a parameter index

        Parameters
        ----------
        solution
            Internal model state for the given |Parameter value|.
        mu
            |Parameter value| for which to solve

        Returns
        -------
        A dict of all partial sensitivities of the solution.
        """
        sensitivities = {}
        for (parameter, size) in self.parameters.items():
            sens_for_param = self.solution_space.empty()
            for l in range(size):
                sens_for_param.append(
                    self._compute_solution_d_mu_single_direction(
                        parameter, l, solution, mu))
            sensitivities[parameter] = sens_for_param
        return sensitivities

    def _compute_output_d_mu(self,
                             solution,
                             mu=None,
                             return_array=False,
                             **kwargs):
        """compute the gradient w.r.t. the parameter of the output functional

        Parameters
        ----------
        solution
            Internal model state for the given |Parameter value|.
        mu
            |Parameter value| for which to compute the gradient
        return_array
            if `True`, return the output gradient as a |NumPy array|.
            Otherwise, return a dict of gradients for each |Parameter|.

        Returns
        -------
        The gradient as a |NumPy array| or a dict of |NumPy arrays|.
        """
        assert self.output_functional is not None
        U_d_mus = self._compute_solution_d_mu(solution, mu)
        gradients = [] if return_array else {}
        for (parameter, size) in self.parameters.items():
            array = np.empty(shape=(size, self.output_functional.range.dim))
            for index in range(size):
                output_partial_dmu = self.output_functional.d_mu(
                    parameter, index).apply(solution, mu=mu).to_numpy()[0]
                U_d_mu = U_d_mus[parameter][index]
                array[
                    index] = output_partial_dmu + self.output_functional.jacobian(
                        solution, mu).apply(U_d_mu, mu).to_numpy()[0]
            if return_array:
                gradients.extend(array)
            else:
                gradients[parameter] = array
        if return_array:
            return np.array(gradients)
        else:
            return gradients

    def _compute_solution_error_estimate(self, solution, mu=None, **kwargs):
        """Compute an error estimate for the computed internal state.

        This method is called by the default implementation of :meth:`compute`
        in :class:`pymor.models.interface.Model`. The assumption is made
        that the error estimate is a derived quantity from the model's internal state
        as returned my :meth:`_compute_solution`. When this is not the case,
        the computation of the error estimate should be implemented in :meth:`_compute`.

        .. note::

            The default implementation calls the `estimate_error` method of the object
            given by the :attr:`error_estimator` attribute, passing `solution`,
            `mu`, `self` and `**kwargs`.

        Parameters
        ----------
        solution
            Internal model state for the given |parameter values|.
        mu
            |Parameter values| for which to compute the error estimate.
        kwargs
            Additional keyword arguments to customize how the error estimate is
            computed or to select additional data to be returned.

        Returns
        -------
        The computed error estimate or a dict which at least must contain the key
        `'solution_error_estimate'`.
        """
        if self.error_estimator is None:
            raise ValueError('Model has no error estimator')
        return self.error_estimator.estimate_error(solution, mu, self,
                                                   **kwargs)

    def _compute_output_error_estimate(self, solution, mu=None, **kwargs):
        """Compute an error estimate for the computed model output.

        This method is called by the default implementation of :meth:`compute`
        in :class:`pymor.models.interface.Model`. The assumption is made
        that the error estimate is a derived quantity from the model's internal state
        as returned my :meth:`_compute_solution`. When this is not the case,
        the computation of the error estimate should be implemented in :meth:`_compute`.

        .. note::

            The default implementation calls the `estimate_output_error` method of the object
            given by the :attr:`error_estimator` attribute, passing `solution`,
            `mu`, `self` and `**kwargs`.

        Parameters
        ----------
        solution
            Internal model state for the given |parameter values|.
        mu
            |Parameter values| for which to compute the error estimate.
        kwargs
            Additional keyword arguments to customize how the error estimate is
            computed or to select additional data to be returned.

        Returns
        -------
        The computed error estimate or a dict which at least must contain the key
        `'solution_error_estimate'`.
        """
        if self.error_estimator is None:
            raise ValueError('Model has no error estimator')
        return self.error_estimator.estimate_output_error(
            solution, mu, self, **kwargs)

    _compute_allowed_kwargs = frozenset()

    def compute(self,
                solution=False,
                output=False,
                solution_d_mu=False,
                output_d_mu=False,
                solution_error_estimate=False,
                output_error_estimate=False,
                output_d_mu_return_array=False,
                *,
                mu=None,
                **kwargs):
        """Compute the solution of the model and associated quantities.

        This methods computes the output of the model it's internal state
        and various associated quantities for given |parameter values|
        `mu`.

        .. note::

            The default implementation defers the actual computations to
            the methods :meth:`_compute_solution`, :meth:`_compute_output`,
            :meth:`_compute_solution_error_estimate` and :meth:`_compute_output_error_estimate`.
            The call to :meth:`_compute_solution` is :mod:`cached <pymor.core.cache>`.
            In addition, |Model| implementors may implement :meth:`_compute` to
            simultaneously compute multiple values in an optimized way. The corresponding
            `_compute_XXX` methods will not be called for values already returned by
            :meth:`_compute`.

        Parameters
        ----------
        solution
            If `True`, return the model's internal state.
        output
            If `True`, return the model output.
        solution_d_mu
            If not `False`, either `True` to return the derivative of the model's
            internal state w.r.t. all parameter components or a tuple `(parameter, index)`
            to return the derivative of a single parameter component.
        output_d_mu
            If `True`, return the gradient of the model output w.r.t. the |Parameter|.
        solution_error_estimate
            If `True`, return an error estimate for the computed internal state.
        output_error_estimate
            If `True`, return an error estimate for the computed output.
        output_d_mu_return_array
            if `True`, return the output gradient as a |NumPy array|.
            Otherwise, return a dict of gradients for each |Parameter|.
        mu
            |Parameter values| for which to compute the values.
        kwargs
            Further keyword arguments to select further quantities that sould
            be returned or to customize how the values are computed.

        Returns
        -------
        A dict with the computed values.
        """

        # make sure no unknown kwargs are passed
        assert kwargs.keys() <= self._compute_allowed_kwargs

        # parse parameter values
        if not isinstance(mu, Mu):
            mu = self.parameters.parse(mu)
        assert self.parameters.assert_compatible(mu)

        # log output
        # explicitly checking if logging is disabled saves some cpu cycles
        if not self.logging_disabled:
            self.logger.info(f'Solving {self.name} for {mu} ...')

        # first call _compute to give subclasses more control
        data = self._compute(solution=solution,
                             output=output,
                             solution_d_mu=solution_d_mu,
                             output_d_mu=output_d_mu,
                             solution_error_estimate=solution_error_estimate,
                             output_error_estimate=output_error_estimate,
                             mu=mu,
                             **kwargs)

        if (solution or output or solution_error_estimate or
                output_error_estimate or solution_d_mu or output_d_mu) \
           and 'solution' not in data:
            retval = self.cached_method_call(self._compute_solution,
                                             mu=mu,
                                             **kwargs)
            if isinstance(retval, dict):
                assert 'solution' in retval
                data.update(retval)
            else:
                data['solution'] = retval

        if output and 'output' not in data:
            # TODO use caching here (requires skipping args in key generation)
            retval = self._compute_output(data['solution'], mu=mu, **kwargs)
            if isinstance(retval, dict):
                assert 'output' in retval
                data.update(retval)
            else:
                data['output'] = retval

        if solution_d_mu and 'solution_d_mu' not in data:
            if isinstance(solution_d_mu, tuple):
                retval = self._compute_solution_d_mu_single_direction(
                    solution_d_mu[0],
                    solution_d_mu[1],
                    data['solution'],
                    mu=mu,
                    **kwargs)
            else:
                retval = self._compute_solution_d_mu(data['solution'],
                                                     mu=mu,
                                                     **kwargs)
            # retval is always a dict
            if isinstance(retval, dict) and 'solution_d_mu' in retval:
                data.update(retval)
            else:
                data['solution_d_mu'] = retval

        if output_d_mu and 'output_d_mu' not in data:
            # TODO use caching here (requires skipping args in key generation)
            retval = self._compute_output_d_mu(
                data['solution'],
                mu=mu,
                return_array=output_d_mu_return_array,
                **kwargs)
            # retval is always a dict
            if isinstance(retval, dict) and 'output_d_mu' in retval:
                data.update(retval)
            else:
                data['output_d_mu'] = retval

        if solution_error_estimate and 'solution_error_estimate' not in data:
            # TODO use caching here (requires skipping args in key generation)
            retval = self._compute_solution_error_estimate(data['solution'],
                                                           mu=mu,
                                                           **kwargs)
            if isinstance(retval, dict):
                assert 'solution_error_estimate' in retval
                data.update(retval)
            else:
                data['solution_error_estimate'] = retval

        if output_error_estimate and 'output_error_estimate' not in data:
            # TODO use caching here (requires skipping args in key generation)
            retval = self._compute_output_error_estimate(data['solution'],
                                                         mu=mu,
                                                         **kwargs)
            if isinstance(retval, dict):
                assert 'output_error_estimate' in retval
                data.update(retval)
            else:
                data['output_error_estimate'] = retval

        return data

    def solve(self, mu=None, return_error_estimate=False, **kwargs):
        """Solve the discrete problem for the |parameter values| `mu`.

        This method returns a |VectorArray| with a internal state
        representation of the model's solution for given
        |parameter values|. It is a convenience wrapper around
        :meth:`compute`.

        The result may be :mod:`cached <pymor.core.cache>`
        in case caching has been activated for the given model.

        Parameters
        ----------
        mu
            |Parameter values| for which to solve.
        return_error_estimate
            If `True`, also return an error estimate for the computed solution.
        kwargs
            Additional keyword arguments passed to :meth:`compute` that
            might affect how the solution is computed.

        Returns
        -------
        The solution |VectorArray|. When `return_error_estimate` is `True`,
        the estimate is returned as second value.
        """
        data = self.compute(solution=True,
                            solution_error_estimate=return_error_estimate,
                            mu=mu,
                            **kwargs)
        if return_error_estimate:
            return data['solution'], data['solution_error_estimate']
        else:
            return data['solution']

    def output(self, mu=None, return_error_estimate=False, **kwargs):
        """Return the model output for given |parameter values| `mu`.

        This method is a convenience wrapper around :meth:`compute`.

        Parameters
        ----------
        mu
            |Parameter values| for which to compute the output.
        return_error_estimate
            If `True`, also return an error estimate for the computed output.
        kwargs
            Additional keyword arguments passed to :meth:`compute` that
            might affect how the solution is computed.

        Returns
        -------
        The computed model output as a 2D |NumPy array|. The dimension
        of axis 1 is :attr:`dim_output`. (For stationary problems, axis 0 has
        dimension 1. For time-dependent problems, the dimension of axis 0
        depends on the number of time steps.)
        When `return_error_estimate` is `True`, the estimate is returned as
        second value.
        """
        data = self.compute(output=True,
                            output_error_estimate=return_error_estimate,
                            mu=mu,
                            **kwargs)
        if return_error_estimate:
            return data['output'], data['output_error_estimate']
        else:
            return data['output']

    def solve_d_mu(self, parameter, index, mu=None, **kwargs):
        """Solve for the partial derivative of the solution w.r.t. a parameter index

        Parameters
        ----------
        parameter
            parameter for which to compute the sensitivity
        index
            parameter index for which to compute the sensitivity
        mu
            |Parameter value| for which to solve

        Returns
        -------
        The sensitivity of the solution as a |VectorArray|.
        """
        data = self.compute(solution_d_mu=(parameter, index), mu=mu, **kwargs)
        return data['solution_d_mu']

    def output_d_mu(self, mu=None, return_array=False, **kwargs):
        """compute the gradient w.r.t. the parameter of the output functional

        Parameters
        ----------
        mu
            |Parameter value| for which to compute the gradient
        return_array
            if `True`, return the output gradient as a |NumPy array|.
            Otherwise, return a dict of gradients for each |Parameter|.

        Returns
        -------
        The gradient as a |NumPy array| or a dict of |NumPy arrays|.
        """
        data = self.compute(output_d_mu=True,
                            mu=mu,
                            output_d_mu_return_array=return_array,
                            **kwargs)
        return data['output_d_mu']

    def estimate_error(self, mu=None, **kwargs):
        """Estimate the error for the computed internal state.

        For given |parameter values| `mu` this method returns an
        error estimate for the computed internal model state as returned
        by :meth:`solve`. It is a convenience wrapper around
        :meth:`compute`.

        The model error could be the error w.r.t. the analytical
        solution of the given problem or the model reduction error w.r.t.
        a corresponding high-dimensional |Model|.

        Parameters
        ----------
        mu
            |Parameter values| for which to estimate the error.
        kwargs
            Additional keyword arguments passed to :meth:`compute` that
            might affect how the error estimate (or the solution) is computed.

        Returns
        -------
        The estimated error.
        """
        return self.compute(solution_error_estimate=True, mu=mu,
                            **kwargs)['solution_error_estimate']

    @Deprecated('estimate_error')
    def estimate(self, U, mu=None):
        return self.estimate_error(mu)

    def estimate_output_error(self, mu=None, **kwargs):
        """Estimate the error for the computed output.

        For given |parameter values| `mu` this method returns an
        error estimate for the computed model output as returned
        by :meth:`output`. It is a convenience wrapper around
        :meth:`compute`.

        The output error could be the error w.r.t. the analytical
        solution of the given problem or the model reduction error w.r.t.
        a corresponding high-dimensional |Model|.

        Parameters
        ----------
        mu
            |Parameter values| for which to estimate the error.
        kwargs
            Additional keyword arguments passed to :meth:`compute` that
            might affect how the error estimate (or the output) is computed.

        Returns
        -------
        The estimated error.
        """
        return self.compute(output_error_estimate=True, mu=mu,
                            **kwargs)['output_error_estimate']

    def visualize(self, U, **kwargs):
        """Visualize a |VectorArray| U of the model's :attr:`solution_space`.

        Parameters
        ----------
        U
            The |VectorArray| from :attr:`solution_space`
            that shall be visualized.
        kwargs
            Additional keyword arguments to customize the visualization.
            See the docstring of `self.visualizer.visualize`.
        """
        if getattr(self, 'visualizer') is not None:
            return self.visualizer.visualize(U, self, **kwargs)
        else:
            raise NotImplementedError('Model has no visualizer.')