Exemple #1
0
    class MockReducedProblem(ParametrizedProblem):
        @sync_setters("truth_problem", "set_mu", "mu")
        @sync_setters("truth_problem", "set_mu_range", "mu_range")
        def __init__(self, truth_problem, **kwargs):
            # Call parent
            ParametrizedProblem.__init__(
                self,
                os.path.join("test_eim_approximation_17_tempdir",
                             expression_type, basis_generation,
                             "mock_problem"))
            # Minimal subset of a ParametrizedReducedDifferentialProblem
            self.truth_problem = truth_problem
            self.basis_functions = BasisFunctionsMatrix(self.truth_problem.V)
            self.basis_functions.init(self.truth_problem.components)
            self._solution = None

        def solve(self):
            print("solving mock reduced problem at mu =", self.mu)
            assert not hasattr(self, "_is_solving")
            self._is_solving = True
            f = self.truth_problem.solve()
            f_N = transpose(
                self.basis_functions) * self.truth_problem.inner_product * f
            # Return the reduced solution
            self._solution = OnlineFunction(f_N)
            delattr(self, "_is_solving")
            return self._solution
Exemple #2
0
def _product(thetas: ThetaType,
             operators: array_of(DelayedBasisFunctionsMatrix), thetas2: None):
    from rbnics.backends import BasisFunctionsMatrix
    space = operators[0].space
    assert all([op.space == space for op in operators])
    components_name = operators[0]._components_name
    assert all([op._components_name == components_name for op in operators])
    output = BasisFunctionsMatrix(space)
    output.init(components_name)
    for component_name in components_name:
        operator_memory_over_basis_functions_index = None  # list (over basis functions index) of list (over theta)
        for operator in operators:
            operator_memory = operator._enrich_memory[
                component_name]  # list (over basis functions index) for current theta
            if operator_memory_over_basis_functions_index is None:
                operator_memory_over_basis_functions_index = [
                    list() for _ in operator_memory
                ]
            assert len(operator_memory_over_basis_functions_index) == len(
                operator_memory)
            for (basis_functions_index,
                 delayed_function) in enumerate(operator_memory):
                operator_memory_over_basis_functions_index[
                    basis_functions_index].append(delayed_function)
        for delayed_functions_over_theta in operator_memory_over_basis_functions_index:
            output.enrich(
                _product(thetas, delayed_functions_over_theta,
                         None).sum_product_return_value,
                component=component_name if len(components_name) > 1 else None)
    return ProductOutput(output)
 def _init_error_estimation_operators(self, current_stage="online"):
     """
     Initialize data structures related to error estimation.
     """
     # Initialize inner product for Riesz solve
     if self._riesz_solve_inner_product is None:  # init was not called already
         self._riesz_solve_inner_product = self.truth_problem._combined_inner_product
     # Setup homogeneous Dirichlet BCs for Riesz solve, if any (no check if init was already called
     # because this variable can actually be None)
     self._riesz_solve_homogeneous_dirichlet_bc = self.truth_problem._combined_and_homogenized_dirichlet_bc
     # Initialize Riesz representation
     for term in self.riesz_terms:
         if term not in self.riesz:  # init was not called already
             self.riesz[term] = self.RieszExpansionStorage(self.Q[term])
             for q in range(self.Q[term]):
                 assert self.terms_order[term] in (1, 2)
                 if self.terms_order[term] > 1:
                     riesz_term_q = BasisFunctionsMatrix(
                         self.truth_problem.V)
                     riesz_term_q.init(self.components)
                 else:
                     riesz_term_q = FunctionsList(
                         self.truth_problem.V)  # will be of size 1
                 self.riesz[term][q] = riesz_term_q
     assert current_stage in ("online", "offline")
     if current_stage == "online":
         for term in self.riesz_terms:
             self.riesz[term].load(self.folder["error_estimation"],
                                   "riesz_" + term)
     elif current_stage == "offline":
         pass  # Nothing else to be done
     else:
         raise ValueError(
             "Invalid stage in _init_error_estimation_operators().")
     # Initialize inner product for Riesz products. This is the same as the inner product for Riesz solves
     # but setting to zero rows & columns associated to boundary conditions
     if self._error_estimation_inner_product is None:  # init was not called already
         if self._riesz_solve_homogeneous_dirichlet_bc is not None:
             self._error_estimation_inner_product = (
                 self._riesz_solve_inner_product
                 & ~self._riesz_solve_homogeneous_dirichlet_bc)
         else:
             self._error_estimation_inner_product = self._riesz_solve_inner_product
     # Initialize error estimation operators
     for term in self.error_estimation_terms:
         if term not in self.error_estimation_operator:  # init was not called already
             self.error_estimation_operator[
                 term] = self.ErrorEstimationOperatorExpansionStorage(
                     self.Q[term[0]], self.Q[term[1]])
     assert current_stage in ("online", "offline")
     if current_stage == "online":
         for term in self.error_estimation_terms:
             self.assemble_error_estimation_operators(term, "online")
     elif current_stage == "offline":
         pass  # Nothing else to be done
     else:
         raise ValueError(
             "Invalid stage in _init_error_estimation_operators().")
def read_basis_functions(W, N):
    basis_functions = BasisFunctionsMatrix(W)
    basis_functions.init(components)
    loaded = basis_functions.load("basis", "basis")
    assert loaded
    N_dict = OnlineSizeDict()
    for c in components:
        N_dict[c] = N
    return basis_functions[:N_dict]
Exemple #5
0
 def generate_random(self):
     # Generate random vectors
     Z = BasisFunctionsMatrix(self.V)
     Z.init("u")
     for _ in range(self.N):
         b = RandomDolfinFunction(self.V)
         Z.enrich(b)
     F = RandomDolfinFunction(self.V)
     # Return
     return (Z, F)
Exemple #6
0
 def generate_random(self):
     # Generate random vectors
     Z = BasisFunctionsMatrix(self.V)
     Z.init("u")
     for i in range(self.N):
         b = RandomDolfinFunction(self.V)
         Z.enrich(b)
     # Generate random matrix
     k = RandomDolfinFunction(self.V)
     A = assemble(self.a(k))
     # Generate random function
     z = RandomDolfinFunction(self.V)
     # Return
     return (Z, A, z.vector())
Exemple #7
0
def perform_POD(N):
    # export mesh - instead of generating mesh everytime
    (mesh, _, _, restrictions) = read_mesh()
    W = generate_block_function_space(mesh, restrictions)

    # POD objects
    X = get_inner_products(W, "POD")
    POD = {c: ProperOrthogonalDecomposition(W, X[c]) for c in components}

    # Solution storage
    solution = BlockFunction(W)

    # Training set
    training_set = get_set("training_set")

    # Read in snapshots
    for mu in training_set:
        print("Appending solution for mu =", mu, "to snapshots matrix")
        read_solution(mu, "truth_solve", solution)
        for c in components:
            POD[c].store_snapshot(solution, component=c)

    # Compress component by component
    basis_functions_component = dict()
    for c in components:
        _, _, basis_functions_component[c], N_c = POD[c].apply(N, tol=0.)
        assert N_c == N
        print("Eigenvalues for component", c)
        POD[c].print_eigenvalues(N)
        POD[c].save_eigenvalues_file("basis", "eigenvalues_" + c)

    # Collect all components and save to file
    basis_functions = BasisFunctionsMatrix(W)
    basis_functions.init(components)
    for c in components:
        basis_functions.enrich(basis_functions_component[c], component=c)
    basis_functions.save("basis", "basis")
    # Also save components to file, for the sake of the ParaView plugin
    with open(os.path.join("basis", "components"), "w") as file_:
        for c in components:
            file_.write(c + "\n")
        def _offline(self):
            # Change default online solve arguments during offline stage to use online stabilization
            # instead of vanishing viscosity one (which will be prepared in a postprocessing stage)
            self.reduced_problem._online_solve_default_kwargs[
                "online_stabilization"] = True
            self.reduced_problem._online_solve_default_kwargs[
                "online_vanishing_viscosity"] = False
            self.reduced_problem.OnlineSolveKwargs = OnlineSolveKwargsGenerator(
                **self.reduced_problem._online_solve_default_kwargs)

            # Call standard offline phase
            EllipticCoerciveReductionMethod_DerivedClass._offline(self)

            # Start vanishing viscosity postprocessing
            print(
                TextBox(
                    self.truth_problem.name() + " " + self.label +
                    " offline vanishing viscosity postprocessing phase begins",
                    fill="="))
            print("")

            # Prepare storage for copy of lifting basis functions matrix
            lifting_basis_functions = BasisFunctionsMatrix(
                self.truth_problem.V)
            lifting_basis_functions.init(self.truth_problem.components)
            # Copy current lifting basis functions to lifting_basis_functions
            N_bc = self.reduced_problem.N_bc
            for i in range(N_bc):
                lifting_basis_functions.enrich(
                    self.reduced_problem.basis_functions[i])
            # Prepare storage for unrotated basis functions matrix, without lifting
            unrotated_basis_functions = BasisFunctionsMatrix(
                self.truth_problem.V)
            unrotated_basis_functions.init(self.truth_problem.components)
            # Copy current basis functions (except lifting) to unrotated_basis_functions
            N = self.reduced_problem.N
            for i in range(N_bc, N):
                unrotated_basis_functions.enrich(
                    self.reduced_problem.basis_functions[i])

            # Prepare new storage for non-hierarchical basis functions matrix and
            # corresponding affine expansions
            self.reduced_problem.init(
                "offline_vanishing_viscosity_postprocessing")

            # Rotated basis functions matrix are not hierarchical, i.e. a different
            # rotation will be applied for each basis size n.
            for n in range(1, N + 1):
                # Prepare storage for rotated basis functions matrix
                rotated_basis_functions = BasisFunctionsMatrix(
                    self.truth_problem.V)
                rotated_basis_functions.init(self.truth_problem.components)
                # Rotate basis
                print("rotate basis functions matrix for n =", n)
                truth_operator_k = self.truth_problem.operator["k"]
                truth_operator_m = self.truth_problem.operator["m"]
                assert len(truth_operator_k) == 1
                assert len(truth_operator_m) == 1
                reduced_operator_k = (
                    transpose(unrotated_basis_functions[:n]) *
                    truth_operator_k[0] * unrotated_basis_functions[:n])
                reduced_operator_m = (
                    transpose(unrotated_basis_functions[:n]) *
                    truth_operator_m[0] * unrotated_basis_functions[:n])
                rotation_eigensolver = OnlineEigenSolver(
                    unrotated_basis_functions[:n], reduced_operator_k,
                    reduced_operator_m)
                parameters = {
                    "problem_type": "hermitian",
                    "spectrum": "smallest real"
                }
                rotation_eigensolver.set_parameters(parameters)
                rotation_eigensolver.solve()
                # Store and save rotated basis
                rotation_eigenvalues = ExportableList("text")
                rotation_eigenvalues.extend([
                    rotation_eigensolver.get_eigenvalue(i)[0] for i in range(n)
                ])
                for i in range(0, n):
                    print("lambda_" + str(i) + " = " +
                          str(rotation_eigenvalues[i]))
                rotation_eigenvalues.save(self.folder["post_processing"],
                                          "rotation_eigs_n=" + str(n))
                for i in range(N_bc):
                    rotated_basis_functions.enrich(lifting_basis_functions[i])
                for i in range(0, n):
                    (eigenvector_i,
                     _) = rotation_eigensolver.get_eigenvector(i)
                    rotated_basis_functions.enrich(
                        unrotated_basis_functions[:n] * eigenvector_i)
                self.reduced_problem.basis_functions[:
                                                     n] = rotated_basis_functions
                # Attach eigenvalues to the vanishing viscosity reduced operator
                self.reduced_problem.vanishing_viscosity_eigenvalues.append(
                    rotation_eigenvalues)

            # Save basis functions
            self.reduced_problem.basis_functions.save(
                self.reduced_problem.folder["basis"], "basis")

            # Re-compute all reduced operators, since the basis functions have changed
            print("build reduced operators")
            self.reduced_problem.build_reduced_operators(
                "offline_vanishing_viscosity_postprocessing")

            # Clean up reduced solution and output cache, since the basis has changed
            self.reduced_problem._solution_cache.clear()
            self.reduced_problem._output_cache.clear()

            print(
                TextBox(
                    self.truth_problem.name() + " " + self.label +
                    " offline vanishing viscosity postprocessing phase ends",
                    fill="="))
            print("")

            # Restore default online solve arguments for online stage
            self.reduced_problem._online_solve_default_kwargs[
                "online_stabilization"] = False
            self.reduced_problem._online_solve_default_kwargs[
                "online_vanishing_viscosity"] = True
            self.reduced_problem.OnlineSolveKwargs = OnlineSolveKwargsGenerator(
                **self.reduced_problem._online_solve_default_kwargs)
Exemple #9
0
 def load(self, directory, filename):
     from rbnics.backends import BasisFunctionsMatrix
     if self._type != "empty":  # avoid loading multiple times
         if self._type in ("basis_functions_matrix", "functions_list"):
             delayed_functions = self._content[self._type]
             it = NonAffineExpansionStorageContent_Iterator(
                 delayed_functions,
                 flags=["c_index", "multi_index", "refs_ok"],
                 op_flags=["readonly"])
             while not it.finished:
                 if isinstance(delayed_functions[it.multi_index],
                               DelayedFunctionsList):
                     assert self._type == "functions_list"
                     if len(
                             delayed_functions[it.multi_index]
                     ) > 0:  # ... unless it is an empty FunctionsList
                         return False
                 elif isinstance(delayed_functions[it.multi_index],
                                 DelayedBasisFunctionsMatrix):
                     assert self._type == "basis_functions_matrix"
                     if sum(
                             delayed_functions[it.multi_index].
                             _component_name_to_basis_component_length.
                             values()
                     ) > 0:  # ... unless it is an empty BasisFunctionsMatrix
                         return False
                 else:
                     raise TypeError("Invalid delayed functions")
                 it.iternext()
         else:
             return False
     # Get full directory name
     full_directory = Folders.Folder(os.path.join(str(directory), filename))
     # Detect trivial case
     assert TypeIO.exists_file(full_directory, "type")
     imported_type = TypeIO.load_file(full_directory, "type")
     self._type = imported_type
     assert self._type in ("basis_functions_matrix", "empty",
                           "error_estimation_operators_11",
                           "error_estimation_operators_21",
                           "error_estimation_operators_22",
                           "functions_list", "operators")
     if self._type in ("basis_functions_matrix", "functions_list"):
         # Load delayed functions
         assert self._type in self._content
         delayed_functions = self._content[self._type]
         it = NonAffineExpansionStorageContent_Iterator(
             delayed_functions, flags=["c_index", "multi_index", "refs_ok"])
         while not it.finished:
             delayed_function = delayed_functions[it.multi_index]
             delayed_function.load(full_directory,
                                   "delayed_functions_" + str(it.index))
             it.iternext()
     elif self._type == "empty":
         pass
     elif self._type in ("error_estimation_operators_11",
                         "error_estimation_operators_21",
                         "error_estimation_operators_22"):
         # Load delayed functions
         assert "delayed_functions" not in self._content
         self._content["delayed_functions"] = [
             NonAffineExpansionStorageContent_Base(self._shape[0],
                                                   dtype=object),
             NonAffineExpansionStorageContent_Base(self._shape[1],
                                                   dtype=object)
         ]
         for (index, delayed_functions) in enumerate(
                 self._content["delayed_functions"]):
             it = NonAffineExpansionStorageContent_Iterator(
                 delayed_functions, flags=["c_index", "refs_ok"])
             while not it.finished:
                 assert DelayedFunctionsTypeIO.exists_file(
                     full_directory, "delayed_functions_" + str(index) +
                     "_" + str(it.index) + "_type")
                 delayed_function_type = DelayedFunctionsTypeIO.load_file(
                     full_directory, "delayed_functions_" + str(index) +
                     "_" + str(it.index) + "_type")
                 assert DelayedFunctionsProblemNameIO.exists_file(
                     full_directory, "delayed_functions_" + str(index) +
                     "_" + str(it.index) + "_problem_name")
                 delayed_function_problem_name = DelayedFunctionsProblemNameIO.load_file(
                     full_directory, "delayed_functions_" + str(index) +
                     "_" + str(it.index) + "_problem_name")
                 delayed_function_problem = get_problem_from_problem_name(
                     delayed_function_problem_name)
                 assert delayed_function_type in (
                     "DelayedBasisFunctionsMatrix", "DelayedLinearSolver")
                 if delayed_function_type == "DelayedBasisFunctionsMatrix":
                     delayed_function = DelayedBasisFunctionsMatrix(
                         delayed_function_problem.V)
                     delayed_function.init(
                         delayed_function_problem.components)
                 elif delayed_function_type == "DelayedLinearSolver":
                     delayed_function = DelayedLinearSolver()
                 else:
                     raise ValueError("Invalid delayed function")
                 delayed_function.load(
                     full_directory, "delayed_functions_" + str(index) +
                     "_" + str(it.index) + "_content")
                 delayed_functions[it.index] = delayed_function
                 it.iternext()
         # Load inner product
         assert ErrorEstimationInnerProductIO.exists_file(
             full_directory, "inner_product_matrix_problem_name")
         inner_product_matrix_problem_name = ErrorEstimationInnerProductIO.load_file(
             full_directory, "inner_product_matrix_problem_name")
         inner_product_matrix_problem = get_problem_from_problem_name(
             inner_product_matrix_problem_name)
         inner_product_matrix_reduced_problem = get_reduced_problem_from_problem(
             inner_product_matrix_problem)
         self._content[
             "inner_product_matrix"] = inner_product_matrix_reduced_problem._error_estimation_inner_product
         # Recompute shape
         assert "delayed_functions_shape" not in self._content
         self._content["delayed_functions_shape"] = DelayedTransposeShape(
             (self._content["delayed_functions"][0][0],
              self._content["delayed_functions"][1][0]))
         # Prepare precomputed slices
         self._precomputed_slices = dict()
         self._prepare_trivial_precomputed_slice()
     elif self._type == "empty":
         pass
     elif self._type == "operators":
         # Load truth content
         assert "truth_operators" not in self._content
         self._content[
             "truth_operators"] = NonAffineExpansionStorageContent_Base(
                 self._shape, dtype=object)
         it = NonAffineExpansionStorageContent_Iterator(
             self._content["truth_operators"],
             flags=["c_index", "multi_index", "refs_ok"])
         while not it.finished:
             assert TruthContentItemIO.exists_file(
                 full_directory,
                 "truth_operator_" + str(it.index) + "_type")
             operator_type = TruthContentItemIO.load_file(
                 full_directory,
                 "truth_operator_" + str(it.index) + "_type")
             assert operator_type in ("NumericForm",
                                      "ParametrizedTensorFactory")
             if operator_type == "NumericForm":
                 assert TruthContentItemIO.exists_file(
                     full_directory, "truth_operator_" + str(it.index))
                 value = TruthContentItemIO.load_file(
                     full_directory, "truth_operator_" + str(it.index))
                 self._content["truth_operators"][
                     it.multi_index] = NumericForm(value)
             elif operator_type == "ParametrizedTensorFactory":
                 assert TruthContentItemIO.exists_file(
                     full_directory, "truth_operator_" + str(it.index))
                 (problem_name, term, index) = TruthContentItemIO.load_file(
                     full_directory, "truth_operator_" + str(it.index))
                 truth_problem = get_problem_from_problem_name(problem_name)
                 self._content["truth_operators"][
                     it.multi_index] = truth_problem.operator[term][index]
             else:
                 raise ValueError("Invalid operator type")
             it.iternext()
         assert "truth_operators_as_expansion_storage" not in self._content
         self._prepare_truth_operators_as_expansion_storage()
         # Load basis functions content
         assert BasisFunctionsContentLengthIO.exists_file(
             full_directory, "basis_functions_length")
         basis_functions_length = BasisFunctionsContentLengthIO.load_file(
             full_directory, "basis_functions_length")
         assert basis_functions_length in (0, 1, 2)
         assert "basis_functions" not in self._content
         self._content["basis_functions"] = list()
         for index in range(basis_functions_length):
             assert BasisFunctionsProblemNameIO.exists_file(
                 full_directory,
                 "basis_functions_" + str(index) + "_problem_name")
             basis_functions_problem_name = BasisFunctionsProblemNameIO.load_file(
                 full_directory,
                 "basis_functions_" + str(index) + "_problem_name")
             assert BasisFunctionsProblemNameIO.exists_file(
                 full_directory,
                 "basis_functions_" + str(index) + "_components_name")
             basis_functions_components_name = BasisFunctionsProblemNameIO.load_file(
                 full_directory,
                 "basis_functions_" + str(index) + "_components_name")
             basis_functions_problem = get_problem_from_problem_name(
                 basis_functions_problem_name)
             basis_functions_reduced_problem = get_reduced_problem_from_problem(
                 basis_functions_problem)
             basis_functions = BasisFunctionsMatrix(
                 basis_functions_reduced_problem.basis_functions.space,
                 basis_functions_components_name
                 if basis_functions_components_name !=
                 basis_functions_problem.components else None)
             basis_functions.init(basis_functions_components_name)
             basis_functions_loaded = basis_functions.load(
                 full_directory,
                 "basis_functions_" + str(index) + "_content")
             assert basis_functions_loaded
             self._content["basis_functions"].append(basis_functions)
         # Recompute shape
         self._content["basis_functions_shape"] = DelayedTransposeShape(
             self._content["basis_functions"])
         # Reset precomputed slices
         self._precomputed_slices = dict()
         self._prepare_trivial_precomputed_slice()
     else:
         raise ValueError("Invalid type")
     return True
Exemple #10
0
class ParametrizedReducedDifferentialProblem(ParametrizedProblem,
                                             metaclass=ABCMeta):
    """
    Base class containing the interface of a projection based ROM for elliptic coercive problems.
    Initialization of dimension of reduced problem N, boundary conditions, terms and their order, number of terms in the affine expansion Q, reduced operators and inner products, reduced solution, reduced basis functions matrix.
    
    :param truth_problem: class of the truth problem to be solved.
    """
    @sync_setters("truth_problem", "set_mu", "mu")
    @sync_setters("truth_problem", "set_mu_range", "mu_range")
    def __init__(self, truth_problem, **kwargs):

        # Call to parent
        ParametrizedProblem.__init__(self, truth_problem.name())

        # $$ ONLINE DATA STRUCTURES $$ #
        # Online reduced space dimension
        self.N = None  # integer (for problems with one component) or dict of integers (for problem with several components)
        self.N_bc = None  # integer (for problems with one component) or dict of integers (for problem with several components)
        self.dirichlet_bc = None  # bool (for problems with one component) or dict of bools (for problem with several components)
        self.dirichlet_bc_are_homogeneous = None  # bool (for problems with one component) or dict of bools (for problem with several components)
        self._combined_and_homogenized_dirichlet_bc = None
        # Form names and order
        self.terms = truth_problem.terms
        self.terms_order = truth_problem.terms_order
        self.components = truth_problem.components
        # Number of terms in the affine expansion
        self.Q = dict()  # from string to integer
        # Reduced order operators
        self.OperatorExpansionStorage = OnlineAffineExpansionStorage
        self.operator = dict()  # from string to OperatorExpansionStorage
        self.inner_product = None  # AffineExpansionStorage (for problems with one component) or dict of AffineExpansionStorage (for problem with several components), even though it will contain only one matrix
        self._combined_inner_product = None
        self.projection_inner_product = None  # AffineExpansionStorage (for problems with one component) or dict of AffineExpansionStorage (for problem with several components), even though it will contain only one matrix
        self._combined_projection_inner_product = None
        # Solution
        self._solution = None  # OnlineFunction
        self._solution_cache = dict()  # of Functions
        self._output = 0
        self._output_cache = dict()  # of Numbers
        self._output_cache__current_cache_key = None

        # $$ OFFLINE DATA STRUCTURES $$ #
        # High fidelity problem
        self.truth_problem = truth_problem
        # Basis functions matrix
        self.basis_functions = None  # BasisFunctionsMatrix
        # I/O
        self.folder["basis"] = os.path.join(self.folder_prefix, "basis")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")
        self.cache_config = config.get("reduced problems", "cache")

    def init(self, current_stage="online"):
        """
        Initialize data structures required during the online phase.
        """
        self._init_operators(current_stage)
        self._init_inner_products(current_stage)
        self._init_basis_functions(current_stage)

    def _init_operators(self, current_stage="online"):
        """
        Initialize data structures required for the online phase. Internal method.
        """
        for term in self.terms:
            if term not in self.operator:  # init was not called already
                self.Q[term] = self.truth_problem.Q[term]
                self.operator[term] = self.OperatorExpansionStorage(
                    self.Q[term])
        assert current_stage in ("online", "offline")
        if current_stage == "online":
            for term in self.terms:
                self.assemble_operator(term, "online")
        elif current_stage == "offline":
            pass  # Nothing else to be done
        else:
            raise ValueError("Invalid stage in _init_operators().")

    def _init_inner_products(self, current_stage="online"):
        """
        Initialize data structures required for the online phase. Internal method.
        """
        assert current_stage in ("online", "offline")
        if current_stage == "online":
            n_components = len(self.components)
            # Inner products
            if self.inner_product is None:  # init was not called already
                if n_components > 1:
                    inner_product_string = "inner_product_{c}"
                    self.inner_product = dict()
                    for component in self.components:
                        self.inner_product[
                            component] = OnlineAffineExpansionStorage(1)
                        self.assemble_operator(
                            inner_product_string.format(c=component), "online")
                else:
                    self.inner_product = OnlineAffineExpansionStorage(1)
                    self.assemble_operator("inner_product", "online")
                self._combined_inner_product = self._combine_all_inner_products(
                )
            # Projection inner product
            if self.projection_inner_product is None:  # init was not called already
                if n_components > 1:
                    projection_inner_product_string = "projection_inner_product_{c}"
                    self.projection_inner_product = dict()
                    for component in self.components:
                        self.projection_inner_product[
                            component] = OnlineAffineExpansionStorage(1)
                        self.assemble_operator(
                            projection_inner_product_string.format(
                                c=component), "online")
                else:
                    self.projection_inner_product = OnlineAffineExpansionStorage(
                        1)
                    self.assemble_operator("projection_inner_product",
                                           "online")
                self._combined_projection_inner_product = self._combine_all_projection_inner_products(
                )
        elif current_stage == "offline":
            n_components = len(self.components)
            # Inner products
            if self.inner_product is None:  # init was not called already
                if n_components > 1:
                    self.inner_product = dict()
                    for component in self.components:
                        self.inner_product[
                            component] = OnlineAffineExpansionStorage(1)
                else:
                    self.inner_product = OnlineAffineExpansionStorage(1)
            # Projection inner product
            if self.projection_inner_product is None:  # init was not called already
                if n_components > 1:
                    self.projection_inner_product = dict()
                    for component in self.components:
                        self.projection_inner_product[
                            component] = OnlineAffineExpansionStorage(1)
                else:
                    self.projection_inner_product = OnlineAffineExpansionStorage(
                        1)
        else:
            raise ValueError("Invalid stage in _init_inner_products().")

    def _combine_all_inner_products(self):
        if len(self.components) > 1:
            all_inner_products = list()
            for component in self.components:
                assert len(
                    self.inner_product[component]
                ) == 1  # the affine expansion storage contains only the inner product matrix
                all_inner_products.append(self.inner_product[component][0])
            all_inner_products = tuple(all_inner_products)
        else:
            assert len(
                self.inner_product
            ) == 1  # the affine expansion storage contains only the inner product matrix
            all_inner_products = (self.inner_product[0], )
        all_inner_products = OnlineAffineExpansionStorage(all_inner_products)
        all_inner_products_thetas = (1., ) * len(all_inner_products)
        return sum(product(all_inner_products_thetas, all_inner_products))

    def _combine_all_projection_inner_products(self):
        if len(self.components) > 1:
            all_projection_inner_products = list()
            for component in self.components:
                assert len(
                    self.projection_inner_product[component]
                ) == 1  # the affine expansion storage contains only the inner product matrix
                all_projection_inner_products.append(
                    self.projection_inner_product[component][0])
            all_projection_inner_products = tuple(
                all_projection_inner_products)
        else:
            assert len(
                self.projection_inner_product
            ) == 1  # the affine expansion storage contains only the inner product matrix
            all_projection_inner_products = (
                self.projection_inner_product[0], )
        all_projection_inner_products = OnlineAffineExpansionStorage(
            all_projection_inner_products)
        all_projection_inner_products_thetas = (
            1., ) * len(all_projection_inner_products)
        return sum(
            product(all_projection_inner_products_thetas,
                    all_projection_inner_products))

    def _init_basis_functions(self, current_stage="online"):
        """
        Basis functions are initialized. Internal method.
        """
        assert current_stage in ("online", "offline")
        # Initialize basis functions mappings
        if self.basis_functions is None:  # avoid re-initializing basis functions matrix multiple times
            self.basis_functions = BasisFunctionsMatrix(self.truth_problem.V)
            self.basis_functions.init(self.components)
        # Get number of components
        n_components = len(self.components)
        # Get helper strings depending on the number of basis components
        if n_components > 1:
            dirichlet_bc_string = "dirichlet_bc_{c}"

            def has_non_homogeneous_dirichlet_bc(component):
                return self.dirichlet_bc[
                    component] and not self.dirichlet_bc_are_homogeneous[
                        component]

            def get_basis_functions(component):
                return self.basis_functions[component]
        else:
            dirichlet_bc_string = "dirichlet_bc"

            def has_non_homogeneous_dirichlet_bc(component):
                return self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous

            def get_basis_functions(component):
                return self.basis_functions

        # Detect how many theta terms are related to boundary conditions
        assert (self.dirichlet_bc is
                None) == (self.dirichlet_bc_are_homogeneous is None)
        if self.dirichlet_bc is None:  # init was not called already
            dirichlet_bc = dict()
            for component in self.components:
                try:
                    theta_bc = self.compute_theta(
                        dirichlet_bc_string.format(c=component))
                except ValueError:  # there were no Dirichlet BCs to be imposed by lifting
                    dirichlet_bc[component] = False
                else:
                    dirichlet_bc[component] = True
            if n_components == 1:
                self.dirichlet_bc = dirichlet_bc[self.components[0]]
            else:
                self.dirichlet_bc = dirichlet_bc
            self.dirichlet_bc_are_homogeneous = self.truth_problem.dirichlet_bc_are_homogeneous
            assert self._combined_and_homogenized_dirichlet_bc is None
            self._combined_and_homogenized_dirichlet_bc = self._combine_and_homogenize_all_dirichlet_bcs(
            )
        # Load basis functions
        if current_stage == "online":
            basis_functions_loaded = self.basis_functions.load(
                self.folder["basis"], "basis")
            # To properly initialize N and N_bc, detect how many theta terms
            # are related to boundary conditions
            if basis_functions_loaded:
                N = OnlineSizeDict()
                N_bc = OnlineSizeDict()
                for component in self.components:
                    if has_non_homogeneous_dirichlet_bc(component):
                        theta_bc = self.compute_theta(
                            dirichlet_bc_string.format(c=component))
                        N[component] = len(
                            get_basis_functions(component)) - len(theta_bc)
                        N_bc[component] = len(theta_bc)
                    else:
                        N[component] = len(get_basis_functions(component))
                        N_bc[component] = 0
                assert len(N) == len(N_bc)
                assert len(N) > 0
                if len(N) == 1:
                    self.N = N[self.components[0]]
                    self.N_bc = N_bc[self.components[0]]
                else:
                    self.N = N
                    self.N_bc = N_bc
        elif current_stage == "offline":
            # Store the lifting functions in self.basis_functions
            for component in self.components:
                self.assemble_operator(
                    dirichlet_bc_string.format(c=component), "offline"
                )  # no return value from assemble_operator in this case
            # Save basis functions matrix, that contains up to now only lifting functions
            self.basis_functions.save(self.folder["basis"], "basis")
            # Properly fill in self.N_bc
            if n_components == 1:
                self.N = 0
                self.N_bc = len(self.basis_functions)
            else:
                N = OnlineSizeDict()
                N_bc = OnlineSizeDict()
                for component in self.components:
                    N[component] = 0
                    N_bc[component] = len(self.basis_functions[component])
                self.N = N
                self.N_bc = N_bc
            # Note that, however, self.N is not increased, so it will actually contain the number
            # of basis functions without the lifting ones.
        else:
            raise ValueError("Invalid stage in _init_basis_functions().")

    def _combine_and_homogenize_all_dirichlet_bcs(self):
        if len(self.components) > 1:
            all_dirichlet_bcs_thetas = dict()
            for component in self.components:
                if self.dirichlet_bc[
                        component] and not self.dirichlet_bc_are_homogeneous[
                            component]:
                    all_dirichlet_bcs_thetas[component] = (0, ) * len(
                        self.compute_theta("dirichlet_bc_" + component))
            if len(all_dirichlet_bcs_thetas) == 0:
                all_dirichlet_bcs_thetas = None
        else:
            if self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous:
                all_dirichlet_bcs_thetas = (0, ) * len(
                    self.compute_theta("dirichlet_bc"))
            else:
                all_dirichlet_bcs_thetas = None
        return all_dirichlet_bcs_thetas

    def solve(self, N=None, **kwargs):
        """
        Perform an online solve. self.N will be used as matrix dimension if the default value is provided for N.
        
        :param N : Dimension of the reduced problem
        :type N : integer
        :return: reduced solution
        """
        N, kwargs = self._online_size_from_kwargs(N, **kwargs)
        N += self.N_bc
        cache_key = self._cache_key_from_N_and_kwargs(N, **kwargs)
        self._solution = OnlineFunction(N)
        if "RAM" in self.cache_config and cache_key in self._solution_cache:
            log(PROGRESS, "Loading reduced solution from cache")
            assign(self._solution, self._solution_cache[cache_key])
        else:
            log(PROGRESS, "Solving reduced problem")
            assert not hasattr(self, "_is_solving")
            self._is_solving = True
            self._solve(N, **kwargs)
            delattr(self, "_is_solving")
            if "RAM" in self.cache_config:
                self._solution_cache[cache_key] = copy(self._solution)
        return self._solution

    class ProblemSolver(object, metaclass=ABCMeta):
        def __init__(self, problem, N):
            self.problem = problem
            self.N = N

        def bc_eval(self):
            problem = self.problem
            if len(problem.components) > 1:
                all_dirichlet_bcs_thetas = dict()
                for component in problem.components:
                    if problem.dirichlet_bc[
                            component] and not problem.dirichlet_bc_are_homogeneous[
                                component]:
                        all_dirichlet_bcs_thetas[
                            component] = problem.compute_theta(
                                "dirichlet_bc_" + component)
                if len(all_dirichlet_bcs_thetas) == 0:
                    all_dirichlet_bcs_thetas = None
            else:
                if problem.dirichlet_bc and not problem.dirichlet_bc_are_homogeneous:
                    all_dirichlet_bcs_thetas = problem.compute_theta(
                        "dirichlet_bc")
                else:
                    all_dirichlet_bcs_thetas = None
            return all_dirichlet_bcs_thetas

        @abstractmethod
        def solve(self):
            pass

    # Perform an online solve (internal)
    def _solve(self, N, **kwargs):
        problem_solver = self.ProblemSolver(self, N)
        problem_solver.solve()

    def project(self, snapshot, on_dirichlet_bc=True, N=None, **kwargs):
        N, kwargs = self._online_size_from_kwargs(N, **kwargs)
        N += self.N_bc

        # Get truth and reduced inner product matrices for projection
        inner_product = self.truth_problem._combined_projection_inner_product
        inner_product_N = self._combined_projection_inner_product[:N, :N]

        # Get basis
        basis_functions = self.basis_functions[:N]

        # Define storage for projected solution
        projected_snapshot_N = OnlineFunction(N)

        # Project on reduced basis
        if on_dirichlet_bc:
            solver = OnlineLinearSolver(
                inner_product_N, projected_snapshot_N,
                transpose(basis_functions) * inner_product * snapshot)
        else:
            solver = OnlineLinearSolver(
                inner_product_N, projected_snapshot_N,
                transpose(basis_functions) * inner_product * snapshot,
                self._combined_and_homogenized_dirichlet_bc)
        solver.set_parameters(self._linear_solver_parameters)
        solver.solve()
        return projected_snapshot_N

    def compute_output(self):
        """
        
        :return: reduced output
        """
        cache_key = self._output_cache__current_cache_key
        if "RAM" in self.cache_config and cache_key in self._output_cache:
            log(PROGRESS, "Loading reduced output from cache")
            self._output = self._output_cache[cache_key]
        else:
            log(PROGRESS, "Computing reduced output")
            N = self._solution.N
            try:
                self._compute_output(N)
            except ValueError:  # raised by compute_theta if output computation is optional
                self._output = NotImplemented
            if "RAM" in self.cache_config:
                self._output_cache[cache_key] = self._output
        return self._output

    def _compute_output(self, N):
        """
        Perform an online evaluation of the output.
        """
        self._output = NotImplemented

    def _online_size_from_kwargs(self, N, **kwargs):
        return OnlineSizeDict.generate_from_N_and_kwargs(
            self.components, self.N, N, **kwargs)

    def _cache_key_from_N_and_kwargs(self, N, **kwargs):
        """
        Internal method.
        
        :param N: dimension of reduced problem.
        """
        for blacklist in ("components", "inner_product"):
            if blacklist in kwargs:
                del kwargs[blacklist]
        if isinstance(N, dict):
            cache_key = (self.mu, tuple(sorted(N.items())),
                         tuple(sorted(kwargs.items())))
        else:
            assert isinstance(N, int)
            cache_key = (self.mu, N, tuple(sorted(kwargs.items())))
        # Store current cache_key to be used when computing output
        self._output_cache__current_cache_key = cache_key
        # Return
        return cache_key

    def build_reduced_operators(self, current_stage="offline"):
        """
        It asssembles the reduced order affine expansion.
        """
        # Inner products and projection inner products
        self._build_reduced_inner_products(current_stage)
        # Terms
        self._build_reduced_operators(current_stage)

    def _build_reduced_operators(self, current_stage="offline"):
        for term in self.terms:
            self.operator[term] = self.assemble_operator(term, current_stage)

    def _build_reduced_inner_products(self, current_stage="offline"):
        n_components = len(self.components)
        # Inner products
        if n_components > 1:
            inner_product_string = "inner_product_{c}"
            for component in self.components:
                self.inner_product[component] = self.assemble_operator(
                    inner_product_string.format(c=component), current_stage)
        else:
            self.inner_product = self.assemble_operator(
                "inner_product", current_stage)
        self._combined_inner_product = self._combine_all_inner_products()
        # Projection inner product
        if n_components > 1:
            projection_inner_product_string = "projection_inner_product_{c}"
            for component in self.components:
                self.projection_inner_product[
                    component] = self.assemble_operator(
                        projection_inner_product_string.format(c=component),
                        current_stage)
        else:
            self.projection_inner_product = self.assemble_operator(
                "projection_inner_product", current_stage)
        self._combined_projection_inner_product = self._combine_all_projection_inner_products(
        )

    def compute_error(self, **kwargs):
        """
        Returns the function _compute_error() evaluated for the desired parameter.
        
        :return: error between online and offline solutions.
        """
        self.truth_problem.solve(**kwargs)
        return self._compute_error(**kwargs)

    def _compute_error(self, **kwargs):
        """
        It computes the error of the reduced order approximation with respect to the full order one for the current value of mu.
        """
        (components, inner_product
         ) = self._preprocess_compute_error_and_relative_error_kwargs(**kwargs)
        # Storage
        error = dict()
        # Compute the error on the solution
        if len(components) > 0:
            N = self._solution.N
            reduced_solution = self.basis_functions[:N] * self._solution
            truth_solution = self.truth_problem._solution
            error_function = truth_solution - reduced_solution
            for component in components:
                error_norm_squared_component = transpose(
                    error_function) * inner_product[component] * error_function
                assert error_norm_squared_component >= 0. or isclose(
                    error_norm_squared_component, 0.)
                error[component] = sqrt(error_norm_squared_component)
        # Simplify trivial case
        if len(components) == 1:
            error = error[components[0]]
        #
        return error

    def compute_relative_error(self, **kwargs):
        """
        It returns the function _compute_relative_error() evaluated for the desired parameter.
        
        :return: relative error.
        """
        absolute_error = self.compute_error(**kwargs)
        return self._compute_relative_error(absolute_error, **kwargs)

    def _compute_relative_error(self, absolute_error, **kwargs):
        """
        It computes the relative error of the reduced order approximation with respect to the full order one for the current value of mu.
        """
        (components, inner_product
         ) = self._preprocess_compute_error_and_relative_error_kwargs(**kwargs)
        # Handle trivial case from compute_error
        if len(components) == 1:
            absolute_error_ = dict()
            absolute_error_[components[0]] = absolute_error
            absolute_error = absolute_error_
        # Storage
        relative_error = dict()
        # Compute the relative error on the solution
        if len(components) > 0:
            truth_solution = self.truth_problem._solution
            for component in components:
                truth_solution_norm_squared_component = transpose(
                    truth_solution) * inner_product[component] * truth_solution
                assert truth_solution_norm_squared_component >= 0. or isclose(
                    truth_solution_norm_squared_component, 0.)
                if truth_solution_norm_squared_component != 0.:
                    relative_error[
                        component] = absolute_error[component] / sqrt(
                            abs(truth_solution_norm_squared_component))
                else:
                    if absolute_error[component] == 0.:
                        relative_error[component] = 0.
                    else:
                        relative_error[component] = float("NaN")
        # Simplify trivial case
        if len(components) == 1:
            relative_error = relative_error[components[0]]
        #
        return relative_error

    def _preprocess_compute_error_and_relative_error_kwargs(self, **kwargs):
        """
        This function returns the components and the inner products, picking them up from the kwargs or choosing default ones in case they are not defined yet. Internal method.
        
        :return: components and inner_product.
        """
        # Set default components, if needed
        if "components" not in kwargs:
            kwargs["components"] = self.components
        # Set inner product for components, if needed
        if "inner_product" not in kwargs:
            inner_product = dict()
            if len(kwargs["components"]) > 1:
                for component in kwargs["components"]:
                    assert len(
                        self.truth_problem.inner_product[component]) == 1
                    inner_product[
                        component] = self.truth_problem.inner_product[
                            component][0]
            else:
                assert len(self.truth_problem.inner_product) == 1
                inner_product[kwargs["components"]
                              [0]] = self.truth_problem.inner_product[0]
            kwargs["inner_product"] = inner_product
        else:
            assert isinstance(kwargs["inner_product"], dict)
            assert set(kwargs["inner_product"].keys()) == set(
                kwargs["components"])
        #
        return (kwargs["components"], kwargs["inner_product"])

    # Compute the error of the reduced order output with respect to the full order one
    # for the current value of mu
    def compute_error_output(self, **kwargs):
        """
        It returns the function _compute_error_output() evaluated for the desired parameter.
        
        :return: output error.
        """
        self.truth_problem.solve(**kwargs)
        self.truth_problem.compute_output()
        return self._compute_error_output(**kwargs)

    # Internal method for output error computation
    def _compute_error_output(self, **kwargs):
        """
        It computes the output error of the reduced order approximation with respect to the full order one for the current value of mu.
        """
        # Skip if no output defined
        if self._output is NotImplemented:
            assert self.truth_problem._output is NotImplemented
            return NotImplemented
        else:  # Compute the error on the output
            reduced_output = self._output
            truth_output = self.truth_problem._output
            error_output = abs(truth_output - reduced_output)
            return error_output

    # Compute the relative error of the reduced order approximation with respect to the full order one
    # for the current value of mu
    def compute_relative_error_output(self, **kwargs):
        """
        It returns the function _compute_relative_error_output() evaluated for the desired parameter.
        
        :return: relative output error.
        """
        absolute_error_output = self.compute_error_output(**kwargs)
        return self._compute_relative_error_output(absolute_error_output,
                                                   **kwargs)

    # Internal method for output error computation
    def _compute_relative_error_output(self, absolute_error_output, **kwargs):
        """
        It computes the realtive output error of the reduced order approximation with respect to the full order one for the current value of mu.
        """
        # Skip if no output defined
        if self._output is NotImplemented:
            assert self.truth_problem._output is NotImplemented
            assert absolute_error_output is NotImplemented
            return NotImplemented
        else:  # Compute the relative error on the output
            truth_output = abs(self.truth_problem._output)
            if truth_output != 0.:
                return absolute_error_output / truth_output
            else:
                if absolute_error_output == 0.:
                    return 0.
                else:
                    return float("NaN")

    def export_solution(self,
                        folder=None,
                        filename=None,
                        solution=None,
                        component=None,
                        suffix=None):
        """
        It exports reduced solution to file.
        
        :param folder: the folder into which we want to save the solution.
        :param filename: the name of the file to be saved.
        :param solution: the solution to be saved.
        :param component: the component of the of the solution to be saved.
        :param suffix: suffix to add to the name.
        """
        if solution is None:
            solution = self._solution
        N = solution.N
        self.truth_problem.export_solution(folder, filename,
                                           self.basis_functions[:N] * solution,
                                           component, suffix)

    def export_error(self,
                     folder=None,
                     filename=None,
                     component=None,
                     suffix=None,
                     **kwargs):
        self.truth_problem.solve(**kwargs)
        reduced_solution = self.basis_functions[:self._solution.
                                                N] * self._solution
        truth_solution = self.truth_problem._solution
        error_function = truth_solution - reduced_solution
        self.truth_problem.export_solution(folder, filename, error_function,
                                           component, suffix)

    def compute_theta(self, term):
        """
        Return theta multiplicative terms of the affine expansion of the problem.
        
        :param term: the forms of the class of the problem.
        :return: computed thetas.
        """
        return self.truth_problem.compute_theta(term)

    # Assemble the reduced order affine expansion
    def assemble_operator(self, term, current_stage="online"):
        """
        Terms and respective thetas are assembled.
        
        :param term: the forms of the class of the problem.
        :param current_stage: online or offline stage.
        """
        assert current_stage in ("online", "offline")
        if current_stage == "online":  # load from file
            # Note that it would not be needed to return the loaded operator in
            # init(), since it has been already modified in-place. We do this, however,
            # because we want this interface to be compatible with the one in
            # EllipticCoerciveProblem, i.e. we would like to be able to use a reduced
            # problem also as a truth problem for a nested reduction
            if term in self.terms:
                self.operator[term].load(self.folder["reduced_operators"],
                                         "operator_" + term)
                return self.operator[term]
            elif term.startswith("inner_product"):
                component = term.replace("inner_product", "").replace("_", "")
                if component != "":
                    assert component in self.components
                    self.inner_product[component].load(
                        self.folder["reduced_operators"], term)
                    return self.inner_product[component]
                else:
                    assert len(self.components) == 1
                    self.inner_product.load(self.folder["reduced_operators"],
                                            term)
                    return self.inner_product
            elif term.startswith("projection_inner_product"):
                component = term.replace("projection_inner_product",
                                         "").replace("_", "")
                if component != "":
                    assert component in self.components
                    self.projection_inner_product[component].load(
                        self.folder["reduced_operators"], term)
                    return self.projection_inner_product[component]
                else:
                    assert len(self.components) == 1
                    self.projection_inner_product.load(
                        self.folder["reduced_operators"], term)
                    return self.projection_inner_product
            elif term.startswith("dirichlet_bc"):
                raise ValueError(
                    "There should be no need to assemble Dirichlet BCs when querying online reduced problems."
                )
            else:
                raise ValueError("Invalid term for assemble_operator().")
        elif current_stage == "offline":
            # As in the previous case, there is no need to return anything because
            # we are still training the reduced order model, so the previous remark
            # (on the usage of a reduced problem as a truth one) cannot hold here.
            # However, in order to have a consistent interface we return the assembled
            # operator
            if term in self.terms:
                assert self.Q[term] == self.truth_problem.Q[term]
                for q in range(self.Q[term]):
                    assert self.terms_order[term] in (0, 1, 2)
                    if self.terms_order[term] == 2:
                        self.operator[term][q] = transpose(
                            self.basis_functions
                        ) * self.truth_problem.operator[term][
                            q] * self.basis_functions
                    elif self.terms_order[term] == 1:
                        self.operator[term][q] = transpose(
                            self.basis_functions
                        ) * self.truth_problem.operator[term][q]
                    elif self.terms_order[term] == 0:
                        self.operator[term][q] = self.truth_problem.operator[
                            term][q]
                    else:
                        raise ValueError("Invalid value for order of term " +
                                         term)
                self.operator[term].save(self.folder["reduced_operators"],
                                         "operator_" + term)
                return self.operator[term]
            elif term.startswith("inner_product"):
                component = term.replace("inner_product", "").replace("_", "")
                if component != "":
                    assert component in self.components
                    assert len(
                        self.inner_product[component]
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    assert len(
                        self.truth_problem.inner_product[component]
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    self.inner_product[component][0] = transpose(
                        self.basis_functions
                    ) * self.truth_problem.inner_product[component][
                        0] * self.basis_functions
                    self.inner_product[component].save(
                        self.folder["reduced_operators"], term)
                    return self.inner_product[component]
                else:
                    assert len(self.components) == 1  # single component case
                    assert len(
                        self.inner_product
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    assert len(
                        self.truth_problem.inner_product
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    self.inner_product[0] = transpose(
                        self.basis_functions
                    ) * self.truth_problem.inner_product[
                        0] * self.basis_functions
                    self.inner_product.save(self.folder["reduced_operators"],
                                            term)
                    return self.inner_product
            elif term.startswith("projection_inner_product"):
                component = term.replace("projection_inner_product",
                                         "").replace("_", "")
                if component != "":
                    assert component in self.components
                    assert len(
                        self.projection_inner_product[component]
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    assert len(
                        self.truth_problem.projection_inner_product[component]
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    self.projection_inner_product[component][0] = transpose(
                        self.basis_functions
                    ) * self.truth_problem.projection_inner_product[component][
                        0] * self.basis_functions
                    self.projection_inner_product[component].save(
                        self.folder["reduced_operators"], term)
                    return self.projection_inner_product[component]
                else:
                    assert len(self.components) == 1  # single component case
                    assert len(
                        self.projection_inner_product
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    assert len(
                        self.truth_problem.projection_inner_product
                    ) == 1  # the affine expansion storage contains only the inner product matrix
                    self.projection_inner_product[0] = transpose(
                        self.basis_functions
                    ) * self.truth_problem.projection_inner_product[
                        0] * self.basis_functions
                    self.projection_inner_product.save(
                        self.folder["reduced_operators"], term)
                    return self.projection_inner_product
            elif term.startswith("dirichlet_bc"):
                component = term.replace("dirichlet_bc", "").replace("_", "")
                if component != "":
                    assert component in self.components
                    has_non_homogeneous_dirichlet_bc = self.dirichlet_bc[
                        component] and not self.dirichlet_bc_are_homogeneous[
                            component]
                else:
                    assert len(self.components) == 1
                    component = None
                    has_non_homogeneous_dirichlet_bc = self.dirichlet_bc and not self.dirichlet_bc_are_homogeneous
                if has_non_homogeneous_dirichlet_bc:
                    # Compute lifting functions for the value of mu possibly provided by the user
                    Q_dirichlet_bcs = len(self.compute_theta(term))
                    # Temporarily override compute_theta method to return only one nonzero
                    # theta term related to boundary conditions
                    standard_compute_theta = self.truth_problem.compute_theta
                    for i in range(Q_dirichlet_bcs):

                        def modified_compute_theta(self, term_):
                            if term_ == term:
                                theta_bc = standard_compute_theta(term_)
                                modified_theta_bc = list()
                                for j in range(Q_dirichlet_bcs):
                                    if j != i:
                                        modified_theta_bc.append(0.)
                                    else:
                                        modified_theta_bc.append(theta_bc[i])
                                return tuple(modified_theta_bc)
                            else:
                                return standard_compute_theta(term_)

                        PatchInstanceMethod(self.truth_problem,
                                            "compute_theta",
                                            modified_compute_theta).patch()
                        # ... and store the solution of the truth problem corresponding to that boundary condition
                        # as lifting function
                        solve_message = "Computing and storing lifting function n. " + str(
                            i)
                        if component is not None:
                            solve_message += " for component " + component
                        solve_message += " (obtained for mu = " + str(
                            self.mu) + ") in the basis matrix"
                        print(solve_message)
                        lifting = self._lifting_truth_solve(term, i)
                        self.basis_functions.enrich(lifting,
                                                    component=component)
                    # Restore the standard compute_theta method
                    self.truth_problem.compute_theta = standard_compute_theta
            else:
                raise ValueError("Invalid term for assemble_operator().")
        else:
            raise ValueError("Invalid stage in assemble_operator().")

    def _lifting_truth_solve(self, term, i):
        # 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 = self.truth_problem.solve(cache_key="lifting_" + str(i))
        lifting /= self.compute_theta(term)[i]
        return lifting

    def get_stability_factor(self):
        """
        Return a lower bound for the coercivity constant.
        """
        return self.truth_problem.get_stability_factor()