예제 #1
0
    class TimeDependentProblem_Class(
            ParametrizedDifferentialProblem_DerivedClass):
        # Default initialization of members
        def __init__(self, V, **kwargs):
            # Call the parent initialization
            ParametrizedDifferentialProblem_DerivedClass.__init__(
                self, V, **kwargs)
            # Store quantities related to the time discretization
            self.t = 0.
            self.t0 = 0.
            self.dt = None
            self.T = None
            # Additional options for time stepping may be stored in the following dict
            self._time_stepping_parameters = dict()
            self._time_stepping_parameters["initial_time"] = self.t0
            # Matrices/vectors resulting from the truth discretization
            # initial_condition: AffineExpansionStorage (for problems with one component) or dict of
            # AffineExpansionStorage (for problem with several components)
            self.initial_condition = None
            # initial_condition_is_homogeneous: bool (for problems with one component) or dict of
            # bools (for problem with several components)
            self.initial_condition_is_homogeneous = None
            # Time derivative of the solution, at the current time
            self._solution_dot = Function(self.V)
            # Solution and output over time
            self._solution_over_time = None  # TimeSeries of Functions
            self._solution_dot_over_time = None  # TimeSeries of Functions
            self._output_over_time = None  # TimeSeries of numbers

            # I/O
            def _solution_cache_key_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_key_from_kwargs(**kwargs)

            def _solution_cache_import(filename):
                solution_over_time = TimeSeries(self._solution_over_time)
                self.import_solution(self.folder["cache"], filename,
                                     solution_over_time)
                return solution_over_time

            def _solution_cache_export(filename, solution, suffix):
                self.export_solution(self.folder["cache"],
                                     filename,
                                     solution,
                                     suffix=suffix)

            def _solution_cache_filename_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_file_from_kwargs(**kwargs)

            self._solution_over_time_cache = TimeSeriesCache(
                "problems",
                key_generator=_solution_cache_key_generator,
                import_=_solution_cache_import,
                export=_solution_cache_export,
                filename_generator=_solution_cache_filename_generator)

            def _solution_dot_cache_key_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_key_from_kwargs(**kwargs)

            def _solution_dot_cache_import(filename):
                solution_dot_over_time = TimeSeries(
                    self._solution_dot_over_time)
                self.import_solution(self.folder["cache"], filename + "_dot",
                                     solution_dot_over_time)
                return solution_dot_over_time

            def _solution_dot_cache_export(filename, solution_dot, suffix):
                self.export_solution(self.folder["cache"],
                                     filename + "_dot",
                                     solution_dot,
                                     suffix=suffix)

            def _solution_dot_cache_filename_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_file_from_kwargs(**kwargs)

            self._solution_dot_over_time_cache = TimeSeriesCache(
                "problems",
                key_generator=_solution_dot_cache_key_generator,
                import_=_solution_dot_cache_import,
                export=_solution_dot_cache_export,
                filename_generator=_solution_dot_cache_filename_generator)
            del self._solution_cache

            def _output_cache_key_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_key_from_kwargs(**kwargs)

            def _output_cache_import(filename):
                output_over_time = list()
                self.import_output(self.folder["cache"], filename,
                                   output_over_time)
                return output_over_time

            def _output_cache_export(filename):
                self.export_output(self.folder["cache"], filename)

            def _output_cache_filename_generator(*args, **kwargs):
                assert len(args) == 1
                assert args[0] == self.mu
                return self._cache_file_from_kwargs(**kwargs)

            self._output_over_time_cache = Cache(
                "problems",
                key_generator=_output_cache_key_generator,
                import_=_output_cache_import,
                export=_output_cache_export,
                filename_generator=_output_cache_filename_generator)
            del self._output_cache

        # Set current time
        def set_time(self, t):
            assert isinstance(t, Number)
            self.t = t

        # Set initial time
        def set_initial_time(self, t0):
            assert isinstance(t0, Number)
            self.t0 = t0
            self._time_stepping_parameters["initial_time"] = t0

        # Set time step size
        def set_time_step_size(self, dt):
            assert isinstance(dt, Number)
            self.dt = dt
            self._time_stepping_parameters["time_step_size"] = dt

        # Set final time
        def set_final_time(self, T):
            assert isinstance(T, Number)
            self.T = T
            self._time_stepping_parameters["final_time"] = T

        # Export solution to file
        def export_solution(self,
                            folder=None,
                            filename=None,
                            solution_over_time=None,
                            component=None,
                            suffix=None):
            if folder is None:
                folder = self.folder_prefix
            if filename is None:
                filename = "solution"
            if solution_over_time is None:
                solution_over_time = self._solution_over_time
            if isinstance(solution_over_time, AbstractTimeSeries):
                assert suffix is None
                for (k, solution) in enumerate(solution_over_time):
                    ParametrizedDifferentialProblem_DerivedClass.export_solution(
                        self,
                        folder,
                        filename,
                        solution,
                        component=component,
                        suffix=k)
            else:
                # Used only for cache export
                solution = solution_over_time
                assert suffix is not None
                ParametrizedDifferentialProblem_DerivedClass.export_solution(
                    self,
                    folder,
                    filename,
                    solution,
                    component=component,
                    suffix=suffix)

        # Import solution from file
        def import_solution(self,
                            folder=None,
                            filename=None,
                            solution_over_time=None,
                            component=None,
                            suffix=None):
            if folder is None:
                folder = self.folder_prefix
            if filename is None:
                filename = "solution"
            if solution_over_time is None:
                solution_over_time = self._solution_over_time
            if isinstance(solution_over_time, AbstractTimeSeries):
                solution = Function(self.V)
                assert suffix is None
                solution_over_time.clear()
                for (k, _) in enumerate(
                        self._solution_over_time.expected_times()):
                    ParametrizedDifferentialProblem_DerivedClass.import_solution(
                        self, folder, filename, solution, component, suffix=k)
                    solution_over_time.append(copy(solution))
            else:
                # Used only for cache import
                solution = solution_over_time
                assert suffix is not None
                ParametrizedDifferentialProblem_DerivedClass.import_solution(
                    self,
                    folder,
                    filename,
                    solution,
                    component=component,
                    suffix=suffix)

        def export_output(self,
                          folder=None,
                          filename=None,
                          output_over_time=None,
                          suffix=None):
            if folder is None:
                folder = self.folder_prefix
            if filename is None:
                filename = "solution"
            if output_over_time is None:
                output_over_time = self._output_over_time
            assert suffix is None
            for (k, output) in enumerate(output_over_time):
                ParametrizedDifferentialProblem_DerivedClass.export_output(
                    self, folder, filename, [output], suffix=k)

        def import_output(self,
                          folder=None,
                          filename=None,
                          output_over_time=None,
                          suffix=None):
            if folder is None:
                folder = self.folder_prefix
            if filename is None:
                filename = "solution"
            output = [0.]
            if output_over_time is None:
                output_over_time = self._output_over_time
            assert suffix is None
            output_over_time.clear()
            for (k, _) in enumerate(self._output_over_time.expected_times()):
                ParametrizedDifferentialProblem_DerivedClass.import_output(
                    self, folder, filename, output, suffix=k)
                assert len(output) == 1
                output_over_time.append(output[0])

        # Initialize data structures required for the offline phase
        def init(self):
            ParametrizedDifferentialProblem_DerivedClass.init(self)
            self._init_initial_condition()
            self._init_time_series()

        def _init_initial_condition(self):
            # Get helper strings depending on the number of basis components
            n_components = len(self.components)
            assert n_components > 0
            if n_components > 1:
                initial_condition_string = "initial_condition_{c}"
            else:
                initial_condition_string = "initial_condition"
            # Assemble initial condition
            # we do not assert for
            # (self.initial_condition is None) == (self.initial_condition_is_homogeneous is None)
            # because self.initial_condition may still be None after initialization, if there
            # were no initial condition at all and the problem had only one component
            if self.initial_condition_is_homogeneous is None:  # init was not called already
                initial_condition = dict()
                initial_condition_is_homogeneous = dict()
                for component in self.components:
                    try:
                        operator_ic = AffineExpansionStorage(
                            self.assemble_operator(
                                initial_condition_string.format(c=component)))
                    except ValueError:  # there were no initial condition: assume homogeneous one
                        initial_condition[component] = None
                        initial_condition_is_homogeneous[component] = True
                    else:
                        initial_condition[component] = operator_ic
                        initial_condition_is_homogeneous[component] = False
                if n_components == 1:
                    self.initial_condition = initial_condition[
                        self.components[0]]
                    self.initial_condition_is_homogeneous = initial_condition_is_homogeneous[
                        self.components[0]]
                else:
                    self.initial_condition = initial_condition
                    self.initial_condition_is_homogeneous = initial_condition_is_homogeneous
                # We enforce consistency between Dirichlet BCs and IC, as in the following cases:
                # a) (homogeneous Dirichlet BCs, homogeneous IC): nothing to be enforced
                # b) (non homogeneous Dirichlet BCs, homogeneous IC): we enforce that the theta
                #     term of each Dirichlet BC is zero at t = 0, resulting in homogeneous Dirichlet BCs
                #     at t = 0. This is needed e.g. in order to make sure that while post processing a snapshot
                #     subtracting the lifting at t = 0 doesn't change the solution (which must remain zero)
                # c) (homogeneous Dirichlet BCs, non homogeneous IC): we trust that the non homogeneous IC
                #     provided by the user is actually zero on the Dirichlet boundaries, otherwise there will
                #     be no way to accurately recover it by projecting on a space which only has bases equal
                #     to zero on the Dirichlet boundary.
                # d) (non homogeneous Dirichlet BCs, non homogeneous IC): we trust that the restriction of IC
                #     on the Dirichlet boundary is equal to the evaluation of the Dirichlet BCs at t = 0.
                #     If that were not true, than post processing a snapshot subtracting the lifting at t = 0
                #     would result in a postprocessed snapshot which is not zero on the Dirichlet boundary, thus
                #     adding an element with non zero value on the Dirichlet boundary to the basis
                for component in self.components:
                    if len(self.components) > 1:
                        has_homogeneous_dirichlet_bc = self.dirichlet_bc_are_homogeneous[
                            component]
                        has_homogeneous_initial_condition = self.initial_condition_is_homogeneous[
                            component]
                        dirichlet_bc_string = "dirichlet_bc_{c}"
                    else:
                        has_homogeneous_dirichlet_bc = self.dirichlet_bc_are_homogeneous
                        has_homogeneous_initial_condition = self.initial_condition_is_homogeneous
                        dirichlet_bc_string = "dirichlet_bc"
                    if has_homogeneous_dirichlet_bc and has_homogeneous_initial_condition:  # case a)
                        pass
                    elif not has_homogeneous_dirichlet_bc and has_homogeneous_initial_condition:  # case b)

                        def generate_modified_compute_theta(component):
                            standard_compute_theta = self.compute_theta

                            def modified_compute_theta(self_, term):
                                if term == dirichlet_bc_string.format(
                                        c=component):
                                    theta_bc = standard_compute_theta(term)
                                    if self_.t == 0.:
                                        return (0., ) * len(theta_bc)
                                    else:
                                        return theta_bc
                                else:
                                    return standard_compute_theta(term)

                            return modified_compute_theta

                        PatchInstanceMethod(
                            self, "compute_theta",
                            generate_modified_compute_theta(
                                component)).patch()
                    elif has_homogeneous_dirichlet_bc and not has_homogeneous_initial_condition:  # case c)
                        pass
                    elif not has_homogeneous_dirichlet_bc and not has_homogeneous_initial_condition:  # case d)
                        pass
                    else:
                        raise RuntimeError("Impossible to arrive here.")

        def _init_time_series(self):
            try:
                monitor_t0 = self._time_stepping_parameters["monitor"][
                    "initial_time"]
            except KeyError:
                monitor_t0 = self.t0
            try:
                monitor_dt = self._time_stepping_parameters["monitor"][
                    "time_step_size"]
            except KeyError:
                assert self.dt is not None
                monitor_dt = self.dt
            assert self.T is not None
            monitor_T = self.T
            self._solution_over_time = TimeSeries((monitor_t0, monitor_T),
                                                  monitor_dt)
            self._solution_dot_over_time = TimeSeries((monitor_t0, monitor_T),
                                                      monitor_dt)
            self._output_over_time = TimeSeries((monitor_t0, monitor_T),
                                                monitor_dt)

        def solve(self, **kwargs):
            self._latest_solve_kwargs = kwargs
            key_error_raised = False
            self._solution_over_time.clear()
            try:
                assign(self._solution_over_time,
                       self._solution_over_time_cache[self.mu, kwargs])
                # **kwargs is not supported by __getitem__
            except KeyError:
                key_error_raised = True
            self._solution_dot_over_time.clear()
            try:
                assign(self._solution_dot_over_time,
                       self._solution_dot_over_time_cache[self.mu, kwargs])
            except KeyError:
                key_error_raised = True
            if key_error_raised:
                # Solutions might still have been loaded from file, only not up to the final time
                assert (
                    len(self._solution_over_time) == len(
                        self._solution_dot_over_time)
                    # simulation was stopped after the solution_dot was written out
                    or len(self._solution_over_time)
                    == len(self._solution_dot_over_time) + 1
                    # simulation was stopped after the solution was written out, but before corresponding
                    # solution_dot was.
                )
                if len(self._solution_over_time) == len(
                        self._solution_dot_over_time) + 1:
                    del self._solution_over_time[-1]
                assert len(self._solution_over_time) == len(
                    self._solution_dot_over_time)
                bak_t0 = self.t0
                if len(self._solution_over_time) == 0:
                    assign(self._solution, Function(self.V))
                    assign(self._solution_dot, Function(self.V))
                else:
                    t0 = self._solution_over_time.stored_times()[-1]
                    self.set_initial_time(t0)
                    assign(self._solution, self._solution_over_time[-1])
                    del self._solution_over_time[-1]
                    assign(self._solution_dot,
                           self._solution_dot_over_time[-1])
                    del self._solution_dot_over_time[-1]
                # Solve
                assert not hasattr(self, "_is_solving")
                self._is_solving = True
                self._solve(**kwargs)
                delattr(self, "_is_solving")
                # Restore initial time, if it was changed
                self.set_initial_time(bak_t0)
            assign(self._solution, self._solution_over_time[-1])
            assign(self._solution_dot, self._solution_dot_over_time[-1])
            return self._solution_over_time

        class ProblemSolver(
                ParametrizedDifferentialProblem_DerivedClass.ProblemSolver,
                TimeDependentProblemWrapper):
            def set_time(self, t):
                problem = self.problem
                problem.set_time(t)

            def bc_eval(self, t):
                assert self.problem.t == t
                return ParametrizedDifferentialProblem_DerivedClass.ProblemSolver.bc_eval(
                    self)

            def ic_eval(self):
                problem = self.problem
                if len(problem.components) > 1:
                    all_initial_conditions = list()
                    all_initial_conditions_thetas = list()
                    for component in problem.components:
                        if problem.initial_condition[component] is not None:
                            all_initial_conditions.extend(
                                problem.initial_condition[component])
                            all_initial_conditions_thetas.extend(
                                problem.compute_theta("initial_condition_" +
                                                      component))
                    if len(all_initial_conditions) > 0:
                        all_initial_conditions = tuple(all_initial_conditions)
                        all_initial_conditions = AffineExpansionStorage(
                            all_initial_conditions)
                        all_initial_conditions_thetas = tuple(
                            all_initial_conditions_thetas)
                    else:
                        all_initial_conditions = None
                        all_initial_conditions_thetas = None
                else:
                    if problem.initial_condition is not None:
                        all_initial_conditions = problem.initial_condition
                        all_initial_conditions_thetas = problem.compute_theta(
                            "initial_condition")
                    else:
                        all_initial_conditions = None
                        all_initial_conditions_thetas = None
                assert (all_initial_conditions is
                        None) == (all_initial_conditions_thetas is None)
                if all_initial_conditions is not None:
                    return sum(
                        product(all_initial_conditions_thetas,
                                all_initial_conditions))
                else:
                    return None

            def monitor(self, t, solution, solution_dot):
                problem = self.problem
                solution_copy = copy(solution)
                problem._solution_over_time.append(solution_copy)
                problem._solution_over_time_cache[
                    problem.mu, self.kwargs].append(solution_copy)
                solution_dot_copy = copy(solution_dot)
                problem._solution_dot_over_time.append(solution_dot_copy)
                problem._solution_dot_over_time_cache[
                    problem.mu, self.kwargs].append(solution_dot_copy)

            def solve(self):
                problem = self.problem
                problem._solution_over_time_cache[
                    problem.mu,
                    self.kwargs] = copy(problem._solution_over_time)
                problem._solution_dot_over_time_cache[
                    problem.mu,
                    self.kwargs] = copy(problem._solution_dot_over_time)
                solver = TimeStepping(self, problem._solution,
                                      problem._solution_dot)
                solver.set_parameters(problem._time_stepping_parameters)
                solver.solve()

        # Perform a truth evaluation of the output
        def compute_output(self):
            """

            :return: output evaluation.
            """
            kwargs = self._latest_solve_kwargs
            try:
                assign(self._output_over_time,
                       self._output_over_time_cache[self.mu, kwargs])
                # **kwargs is not supported by __getitem__
            except KeyError:
                try:
                    self._compute_output()
                except ValueError:  # raised by compute_theta if output computation is optional
                    self._output_over_time.clear()
                    self._output_over_time.extend(
                        [NotImplemented] * len(self._solution_over_time))
                    self._output = NotImplemented
                self._output_over_time_cache[self.mu,
                                             kwargs] = self._output_over_time
            else:
                self._output = self._output_over_time[-1]
            return self._output_over_time

        # Perform a truth evaluation of the output
        def _compute_output(self):
            self._output_over_time.clear()
            self._output_over_time.extend([NotImplemented] *
                                          len(self._solution_over_time))
            self._output = NotImplemented
    class TimeDependentReducedProblem_Class(
            ParametrizedReducedDifferentialProblem_DerivedClass):

        # Default initialization of members
        @sync_setters("truth_problem", "set_time", "t")
        @sync_setters("truth_problem", "set_initial_time", "t0")
        @sync_setters("truth_problem", "set_time_step_size", "dt")
        @sync_setters("truth_problem", "set_final_time", "T")
        def __init__(self, truth_problem, **kwargs):
            # Call the parent initialization
            ParametrizedReducedDifferentialProblem_DerivedClass.__init__(
                self, truth_problem, **kwargs)
            # Store quantities related to the time discretization
            assert truth_problem.t == 0.
            self.t = 0.
            self.t0 = truth_problem.t0
            assert truth_problem.dt is not None
            self.dt = truth_problem.dt
            assert truth_problem.T is not None
            self.T = truth_problem.T
            # Additional options for time stepping may be stored in the following dict
            self._time_stepping_parameters = dict()
            self._time_stepping_parameters["initial_time"] = self.t0
            self._time_stepping_parameters["time_step_size"] = self.dt
            self._time_stepping_parameters["final_time"] = self.T
            if "monitor" in truth_problem._time_stepping_parameters:
                self._time_stepping_parameters[
                    "monitor"] = truth_problem._time_stepping_parameters[
                        "monitor"]
            # Online reduced space dimension
            # initial_condition: bool (for problems with one component) or dict of bools (for problem with
            # several components)
            self.initial_condition = None
            # initial_condition_is_homogeneous: bool (for problems with one component) or dict of bools
            # (for problem with several components)
            self.initial_condition_is_homogeneous = None
            # Number of terms in the affine expansion
            # Q_ic: integer (for problems with one component) or dict of integers (for problem with several components)
            self.Q_ic = None
            # Time derivative of the solution, at the current time
            self._solution_dot = None  # OnlineFunction
            # Solution and output over time
            self._solution_over_time = None  # TimeSeries of Functions
            self._solution_dot_over_time = None  # TimeSeries of Functions
            self._output_over_time = None  # TimeSeries of numbers

            # I/O
            def _solution_cache_key_generator(*args, **kwargs):
                assert len(args) == 2
                assert args[0] == self.mu
                return self._cache_key_from_N_and_kwargs(args[1], **kwargs)

            self._solution_over_time_cache = TimeSeriesCache(
                "reduced problems",
                key_generator=_solution_cache_key_generator)

            self._solution_dot_over_time_cache = TimeSeriesCache(
                "reduced problems",
                key_generator=_solution_cache_key_generator)
            del self._solution_cache

            def _output_cache_key_generator(*args, **kwargs):
                assert len(args) == 2
                assert args[0] == self.mu
                return self._cache_key_from_N_and_kwargs(args[1], **kwargs)

            self._output_over_time_cache = Cache(
                "reduced problems", key_generator=_output_cache_key_generator)
            del self._output_cache

        # Set current time
        def set_time(self, t):
            self.t = t

        # Set initial time
        def set_initial_time(self, t0):
            assert isinstance(t0, Number)
            self.t0 = t0
            self._time_stepping_parameters["initial_time"] = t0

        # Set time step size
        def set_time_step_size(self, dt):
            assert isinstance(dt, Number)
            self.dt = dt
            self._time_stepping_parameters["time_step_size"] = dt

        # Set final time
        def set_final_time(self, T):
            assert isinstance(T, Number)
            self.T = T
            self._time_stepping_parameters["final_time"] = T

        # Initialize data structures required for the online phase
        def init(self, current_stage="online"):
            # Initialize first data structures related to initial conditions
            self._init_initial_condition(current_stage)
            self._init_time_series(current_stage)
            # ... since the Parent call may be overridden to need them!
            ParametrizedReducedDifferentialProblem_DerivedClass.init(
                self, current_stage)

        def _init_initial_condition(self, current_stage="online"):
            assert current_stage in ("online", "offline")
            n_components = len(self.components)
            # Get helper strings depending on the number of components
            if n_components > 1:
                initial_condition_string = "initial_condition_{c}"
            else:
                initial_condition_string = "initial_condition"
            # Detect how many theta terms are related to boundary conditions
            # we do not assert for
            # (self.initial_condition is None) == (self.initial_condition_is_homogeneous is None)
            # because self.initial_condition may still be None after initialization, if there
            # were no initial condition at all and the problem had only one component
            if self.initial_condition_is_homogeneous is None:  # init was not called already
                initial_condition = dict()
                initial_condition_is_homogeneous = dict()
                Q_ic = dict()
                for component in self.components:
                    try:
                        theta_ic = self.compute_theta(
                            initial_condition_string.format(c=component))
                    except ValueError:  # there were no initial condition to be imposed by lifting
                        initial_condition[component] = None
                        initial_condition_is_homogeneous[component] = True
                        Q_ic[component] = 0
                    else:
                        initial_condition_is_homogeneous[component] = False
                        Q_ic[component] = len(theta_ic)
                        initial_condition[
                            component] = OnlineAffineExpansionStorage(
                                Q_ic[component])
                if n_components == 1:
                    self.initial_condition = initial_condition[
                        self.components[0]]
                    self.initial_condition_is_homogeneous = initial_condition_is_homogeneous[
                        self.components[0]]
                    self.Q_ic = Q_ic[self.components[0]]
                else:
                    self.initial_condition = initial_condition
                    self.initial_condition_is_homogeneous = initial_condition_is_homogeneous
                    self.Q_ic = Q_ic
                assert self.initial_condition_is_homogeneous == self.truth_problem.initial_condition_is_homogeneous
                # Load initial conditions from file if we are online
                if current_stage == "online":
                    for component in self.components:
                        if not initial_condition_is_homogeneous[component]:
                            self.assemble_operator(
                                initial_condition_string.format(c=component),
                                "online")
                elif current_stage == "offline":
                    pass  # Nothing else to be done
                else:
                    raise ValueError(
                        "Invalid stage in _init_initial_condition().")

        def _init_time_series(self, current_stage="online"):
            try:
                monitor_t0 = self._time_stepping_parameters["monitor"][
                    "initial_time"]
            except KeyError:
                monitor_t0 = self.t0
            try:
                monitor_dt = self._time_stepping_parameters["monitor"][
                    "time_step_size"]
            except KeyError:
                assert self.dt is not None
                monitor_dt = self.dt
            assert self.T is not None
            monitor_T = self.T
            self._solution_over_time = TimeSeries((monitor_t0, monitor_T),
                                                  monitor_dt)
            self._solution_dot_over_time = TimeSeries((monitor_t0, monitor_T),
                                                      monitor_dt)
            self._output_over_time = TimeSeries((monitor_t0, monitor_T),
                                                monitor_dt)

        # Assemble the reduced order affine expansion.
        def build_reduced_operators(self, current_stage="offline"):
            ParametrizedReducedDifferentialProblem_DerivedClass.build_reduced_operators(
                self, current_stage)
            # Initial condition
            self._build_reduced_initial_condition(current_stage)

        def _build_reduced_initial_condition(self, current_stage="offline"):
            if len(self.components) > 1:
                initial_condition_string = "initial_condition_{c}"
                for component in self.components:
                    if not self.initial_condition_is_homogeneous[component]:
                        self.initial_condition[
                            component] = self.assemble_operator(
                                initial_condition_string.format(c=component),
                                current_stage)
            else:
                if not self.initial_condition_is_homogeneous:
                    self.initial_condition = self.assemble_operator(
                        "initial_condition", "offline")

        # Assemble the reduced order affine expansion
        def assemble_operator(self, term, current_stage="online"):
            assert current_stage in ("online", "offline")
            if term.startswith("initial_condition"):
                component = term.replace("initial_condition",
                                         "").replace("_", "")
                if current_stage == "online":  # load from file
                    if component != "":
                        initial_condition = self.initial_condition[component]
                    else:
                        initial_condition = self.initial_condition
                    initial_condition.load(self.folder["reduced_operators"],
                                           term)
                elif current_stage == "offline":
                    if component != "":
                        truth_initial_condition = self.truth_problem.initial_condition[
                            component]
                        initial_condition = self.initial_condition[component]
                        truth_projection_inner_product = self.truth_problem.projection_inner_product[
                            component]
                    else:
                        truth_initial_condition = self.truth_problem.initial_condition
                        initial_condition = self.initial_condition
                        truth_projection_inner_product = self.truth_problem.projection_inner_product
                    assert len(truth_projection_inner_product) == 1
                    # the affine expansion storage contains only the inner product matrix
                    for (q, truth_initial_condition_q
                         ) in enumerate(truth_initial_condition):
                        initial_condition[q] = (
                            transpose(self.basis_functions) *
                            truth_projection_inner_product[0] *
                            truth_initial_condition_q)
                    initial_condition.save(self.folder["reduced_operators"],
                                           term)
                else:
                    raise ValueError("Invalid stage in assemble_operator().")
                # Assign
                if component != "":
                    assert component in self.components
                    self.initial_condition[component] = initial_condition
                else:
                    assert len(self.components) == 1
                    self.initial_condition = initial_condition
                # Return
                return initial_condition
            else:
                return ParametrizedReducedDifferentialProblem_DerivedClass.assemble_operator(
                    self, term, current_stage)

        def solve(self, N=None, **kwargs):
            N, kwargs = self._online_size_from_kwargs(N, **kwargs)
            N += self.N_bc
            self._latest_solve_kwargs = kwargs
            self._solution = OnlineFunction(N)
            self._solution_over_time.clear()
            self._solution_dot = OnlineFunction(N)
            self._solution_dot_over_time.clear()
            if N == 0:  # trivial case
                self._solution_over_time.extend([
                    self._solution
                    for _ in self._solution_over_time.expected_times()
                ])
                self._solution_dot_over_time.extend([
                    self._solution_dot
                    for _ in self._solution_dot_over_time.expected_times()
                ])
                return self._solution_over_time
            try:
                assign(self._solution_over_time,
                       self._solution_over_time_cache[self.mu, N, kwargs])
                # **kwargs is not supported by __getitem__
                assign(self._solution_dot_over_time,
                       self._solution_dot_over_time_cache[self.mu, N, kwargs])
            except KeyError:
                assert not hasattr(self, "_is_solving")
                self._is_solving = True
                self._solve(N, **kwargs)
                delattr(self, "_is_solving")
            assign(self._solution, self._solution_over_time[-1])
            assign(self._solution_dot, self._solution_dot_over_time[-1])
            return self._solution_over_time

        class ProblemSolver(
                ParametrizedReducedDifferentialProblem_DerivedClass.
                ProblemSolver, TimeDependentProblemWrapper):
            def set_time(self, t):
                problem = self.problem
                problem.set_time(t)

            def bc_eval(self, t):
                assert self.problem.t == t
                return ParametrizedReducedDifferentialProblem_DerivedClass.ProblemSolver.bc_eval(
                    self)

            def ic_eval(self):
                problem = self.problem
                N = self.N
                if len(problem.components) > 1:
                    all_initial_conditions = list()
                    all_initial_conditions_thetas = list()
                    for component in problem.components:
                        if (problem.initial_condition[component]
                                and not problem.
                                initial_condition_is_homogeneous[component]):
                            all_initial_conditions.extend(
                                problem.initial_condition[component][:N])
                            all_initial_conditions_thetas.extend(
                                problem.compute_theta("initial_condition_" +
                                                      component))
                    if len(all_initial_conditions) > 0:
                        all_initial_conditions = tuple(all_initial_conditions)
                        all_initial_conditions = OnlineAffineExpansionStorage(
                            all_initial_conditions)
                        all_initial_conditions_thetas = tuple(
                            all_initial_conditions_thetas)
                    else:
                        all_initial_conditions = None
                        all_initial_conditions_thetas = None
                else:
                    if problem.initial_condition and not problem.initial_condition_is_homogeneous:
                        all_initial_conditions = problem.initial_condition[:N]
                        all_initial_conditions_thetas = problem.compute_theta(
                            "initial_condition")
                    else:
                        all_initial_conditions = None
                        all_initial_conditions_thetas = None
                assert (all_initial_conditions is
                        None) == (all_initial_conditions_thetas is None)
                if all_initial_conditions is not None:
                    inner_product_N = problem._combined_projection_inner_product[:
                                                                                 N, :
                                                                                 N]
                    projected_initial_condition = OnlineFunction(N)
                    solver = OnlineLinearSolver(
                        inner_product_N, projected_initial_condition,
                        sum(
                            product(all_initial_conditions_thetas,
                                    all_initial_conditions)))
                    solver.set_parameters(problem._linear_solver_parameters)
                    solver.solve()
                    return projected_initial_condition
                else:
                    return None

            def monitor(self, t, solution, solution_dot):
                problem = self.problem
                solution_copy = copy(solution)
                problem._solution_over_time.append(solution_copy)
                problem._solution_over_time_cache[
                    problem.mu, self.N, self.kwargs].append(solution_copy)
                solution_dot_copy = copy(solution_dot)
                problem._solution_dot_over_time.append(solution_dot_copy)
                problem._solution_dot_over_time_cache[
                    problem.mu, self.N, self.kwargs].append(solution_dot_copy)

            def solve(self):
                problem = self.problem
                assert len(problem._solution_over_time) == 0
                problem._solution_over_time_cache[
                    problem.mu, self.N,
                    self.kwargs] = copy(problem._solution_over_time)
                assert len(problem._solution_dot_over_time) == 0
                problem._solution_dot_over_time_cache[
                    problem.mu, self.N,
                    self.kwargs] = copy(problem._solution_dot_over_time)
                solver = OnlineTimeStepping(self, problem._solution,
                                            problem._solution_dot)
                solver.set_parameters(problem._time_stepping_parameters)
                solver.solve()

        # Perform an online evaluation of the output
        def compute_output(self):
            N = self._solution.N
            kwargs = self._latest_solve_kwargs
            try:
                assign(self._output_over_time,
                       self._output_over_time_cache[self.mu, N, kwargs])
                # **kwargs is not supported by __getitem__
            except KeyError:
                try:
                    self._compute_output(N)
                except ValueError:  # raised by compute_theta if output computation is optional
                    self._output_over_time.clear()
                    self._output_over_time.extend(
                        [NotImplemented] * len(self._solution_over_time))
                    self._output = NotImplemented
                self._output_over_time_cache[self.mu, N,
                                             kwargs] = self._output_over_time
            else:
                self._output = self._output_over_time[-1]
            return self._output_over_time

        # Perform an online evaluation of the output. Internal method
        def _compute_output(self, N):
            self._output_over_time.clear()
            self._output_over_time.extend([NotImplemented] *
                                          len(self._solution_over_time))
            self._output = NotImplemented

        def _lifting_truth_solve(self, term, i):
            assert term.startswith("dirichlet_bc")
            component = term.replace("dirichlet_bc", "").replace("_", "")
            # Since lifting solves for different values of i are associated to the same parameter
            # but with a patched call to compute_theta(), which returns the i-th component, we set
            # a custom cache_key so that they are properly differentiated when reading from cache.
            lifting_over_time = self.truth_problem.solve(
                cache_key="lifting_" + component + "_" + str(i))
            times = lifting_over_time.stored_times()
            theta_over_time = list()
            for t in times:
                self.truth_problem.set_time(t)
                theta_over_time.append(
                    self.truth_problem.compute_theta(term)[i])
            # We average the time dependent solution to be used as time independent lifting.
            # Do not even bother adding the initial condition if it is zero
            if not isclose(times[0], self.truth_problem.t0,
                           self.truth_problem.dt / 2):
                has_non_homogeneous_initial_condition = True
            else:
                if component != "":
                    assert component in self.truth_problem.components
                    has_non_homogeneous_initial_condition = self.truth_problem.initial_condition[
                        component] and not self.truth_problem.initial_condition_is_homogeneous[
                            component]
                else:
                    assert len(self.truth_problem.components) == 1
                    component = None
                    has_non_homogeneous_initial_condition = (
                        self.truth_problem.initial_condition and
                        not self.truth_problem.initial_condition_is_homogeneous
                    )
            if has_non_homogeneous_initial_condition:
                time_interval = (times[0], times[-1])
            else:
                time_interval = (times[1], times[-1])
                lifting_over_time = lifting_over_time[1:]
                theta_over_time = theta_over_time[1:]
            # Compute the average and return
            lifting_quadrature = TimeQuadrature(time_interval,
                                                lifting_over_time)
            theta_quadrature = TimeQuadrature(time_interval, theta_over_time)
            lifting = lifting_quadrature.integrate()
            lifting /= theta_quadrature.integrate()
            return lifting

        def project(self,
                    snapshot_over_time,
                    N=None,
                    on_dirichlet_bc=True,
                    **kwargs):
            projected_snapshot_N_over_time = TimeSeries(snapshot_over_time)
            for snapshot in snapshot_over_time:
                projected_snapshot_N = ParametrizedReducedDifferentialProblem_DerivedClass.project(
                    self, snapshot, N, on_dirichlet_bc, **kwargs)
                projected_snapshot_N_over_time.append(projected_snapshot_N)
            return projected_snapshot_N_over_time

        # Internal method for error computation
        def _compute_error(self, **kwargs):
            error_over_time = TimeSeries(self._solution_over_time)
            assert len(self.truth_problem._solution_over_time) == len(
                self._solution_over_time)
            for (k, t) in enumerate(
                    self.truth_problem._solution_over_time.stored_times()):
                self.set_time(t)
                assign(self._solution, self._solution_over_time[k])
                assign(self.truth_problem._solution,
                       self.truth_problem._solution_over_time[k])
                error = ParametrizedReducedDifferentialProblem_DerivedClass._compute_error(
                    self, **kwargs)
                error_over_time.append(error)
            error_over_time = self._convert_error_over_time(error_over_time)
            return error_over_time

        # Internal method for relative error computation
        def _compute_relative_error(self, absolute_error_over_time, **kwargs):
            relative_error_over_time = TimeSeries(self._solution_over_time)
            if isinstance(absolute_error_over_time, dict):
                for (_, absolute_error_over_time_for_component
                     ) in absolute_error_over_time.items():
                    assert len(self._solution_over_time) == len(
                        absolute_error_over_time_for_component)
            else:
                assert len(
                    self._solution_over_time) == len(absolute_error_over_time)
            for (k, t) in enumerate(
                    self.truth_problem._solution_over_time.stored_times()):
                self.set_time(t)
                assign(self.truth_problem._solution,
                       self.truth_problem._solution_over_time[k])
                absolute_error = self._convert_error_at_time(
                    k, absolute_error_over_time)
                relative_error = ParametrizedReducedDifferentialProblem_DerivedClass._compute_relative_error(
                    self, absolute_error, **kwargs)
                relative_error_over_time.append(relative_error)
            relative_error_over_time = self._convert_error_over_time(
                relative_error_over_time)
            return relative_error_over_time

        # Internal method for output error computation
        def _compute_error_output(self, **kwargs):
            error_output_over_time = TimeSeries(self._output_over_time)
            assert len(self.truth_problem._output_over_time) == len(
                self._output_over_time)
            for (k, t) in enumerate(
                    self.truth_problem._output_over_time.stored_times()):
                self.set_time(t)
                self._output = self._output_over_time[k]
                self.truth_problem._output = self.truth_problem._output_over_time[
                    k]
                error_output = ParametrizedReducedDifferentialProblem_DerivedClass._compute_error_output(
                    self, **kwargs)
                error_output_over_time.append(error_output)
            return error_output_over_time

        # Internal method for output relative error computation
        def _compute_relative_error_output(self,
                                           absolute_error_output_over_time,
                                           **kwargs):
            relative_error_output_over_time = TimeSeries(
                self._output_over_time)
            assert len(self.truth_problem._output_over_time) == len(
                absolute_error_output_over_time)
            for (k, t) in enumerate(
                    self.truth_problem._output_over_time.stored_times()):
                self.set_time(t)
                self.truth_problem._output = self.truth_problem._output_over_time[
                    k]
                relative_error_output = (
                    ParametrizedReducedDifferentialProblem_DerivedClass.
                    _compute_relative_error_output(
                        self, absolute_error_output_over_time[k], **kwargs))
                relative_error_output_over_time.append(relative_error_output)
            return relative_error_output_over_time

        def _convert_error_over_time(self, error_over_time):
            """
            This internal method converts the error over time as follows:
             - if the problem has only one component, no conversion is required;
             - if the problem has more than one component, then a conversion is preformed such that
               the result is a dict from components to error over time for that component, rather than
               a list (over time) of dicts (over components)
            """
            assert isinstance(error_over_time[0], (dict, Number))
            if isinstance(error_over_time[0], dict):
                assert all(
                    [isinstance(error, dict) for error in error_over_time])
                components = list(error_over_time[0].keys())
                assert all([
                    list(error.keys()) == components
                    for error in error_over_time
                ])
                output = dict()
                for component in components:
                    output[component] = TimeSeries(error_over_time)
                    for error in error_over_time:
                        output[component].append(error[component])
                return output
            else:
                assert all(
                    [isinstance(error, Number) for error in error_over_time])
                return error_over_time

        def _convert_error_at_time(self, k, converted_error_over_time):
            """
            This internal method converts back the error at time step k as follows:
             - if the problem has only one component, no conversion is required, and the error at time
               k is returned by extracting the k-th index of the input;
             - if the problem has more than one component, then a conversion is performed such that
               the result is a dict from components to error at time step k
            """
            assert (
                isinstance(converted_error_over_time, dict)
                or hasattr(converted_error_over_time, "__iter__")
                # replaces isinstance(converted_error_over_time, TimeSeries)
            )
            if isinstance(converted_error_over_time, dict):
                output = dict()
                for (component, error_over_time_for_component
                     ) in converted_error_over_time.items():
                    assert all([
                        isinstance(error, Number)
                        for error in error_over_time_for_component
                    ])
                    output[component] = error_over_time_for_component[k]
                return output
            else:
                assert all([
                    isinstance(error, Number)
                    for error in converted_error_over_time
                ])
                return converted_error_over_time[k]

        # Export solution to file
        def export_solution(self,
                            folder=None,
                            filename=None,
                            solution_over_time=None,
                            component=None,
                            suffix=None):
            if solution_over_time is None:
                solution_over_time = self._solution_over_time
            for (k, solution) in enumerate(solution_over_time):
                N = solution.N
                assert suffix is None
                self.truth_problem.export_solution(folder,
                                                   filename,
                                                   self.basis_functions[:N] *
                                                   solution,
                                                   component=component,
                                                   suffix=k)

        def export_error(self,
                         folder=None,
                         filename=None,
                         component=None,
                         suffix=None,
                         **kwargs):
            self.truth_problem.solve(**kwargs)
            assert len(self.truth_problem._solution_over_time) == len(
                self._solution_over_time)
            for (k, t) in enumerate(
                    self.truth_problem._solution_over_time.stored_times()):
                self.set_time(t)
                assign(self._solution, self._solution_over_time[k])
                assign(self.truth_problem._solution,
                       self.truth_problem._solution_over_time[k])
                assert suffix is None
                self.truth_problem.export_solution(
                    folder,
                    filename,
                    self.truth_problem._solution -
                    self.basis_functions[:self._solution.N] * self._solution,
                    component=component,
                    suffix=k)

        def export_output(self,
                          folder=None,
                          filename=None,
                          output_over_time=None,
                          suffix=None):
            self.truth_problem.export_output(folder, filename,
                                             output_over_time, suffix)