Пример #1
0
 def _load_content(self, N, directory, filename):
     affine_expansion_N = OnlineAffineExpansionStorage(self._len)
     loaded = affine_expansion_N.load(directory, filename + "_N=" + str(N))
     assert loaded is True
     return affine_expansion_N
Пример #2
0
    class TimeDependentRBReducedProblem_Class(
            ParametrizedReducedDifferentialProblem_DerivedClass):

        # Default initialization of members.
        def __init__(self, truth_problem, **kwargs):
            # Call to parent
            ParametrizedReducedDifferentialProblem_DerivedClass.__init__(
                self, truth_problem, **kwargs)

            # Storage related to error estimation for initial condition
            # initial_condition_product: AffineExpansionStorage (for problems with one component)
            # or dict of AffineExpansionStorage (for problem with several components)
            self.initial_condition_product = None

        def _init_error_estimation_operators(self, current_stage="online"):
            ParametrizedReducedDifferentialProblem_DerivedClass._init_error_estimation_operators(
                self, current_stage)
            # Also initialize data structures related to initial condition error estimation
            if len(self.components) > 1:
                initial_condition_product = dict()
                for component in self.components:
                    if self.initial_condition[
                            component] and not self.initial_condition_is_homogeneous[
                                component]:
                        initial_condition_product[
                            component,
                            component] = OnlineAffineExpansionStorage(
                                self.Q_ic[component], self.Q_ic[component])
                        assert current_stage in ("online", "offline")
                        if current_stage == "online":
                            self.assemble_error_estimation_operators(
                                ("initial_condition_" + component,
                                 "initial_condition_" + component), "online")
                        elif current_stage == "offline":
                            pass  # Nothing else to be done
                        else:
                            raise ValueError(
                                "Invalid stage in _init_error_estimation_operators()."
                            )
                if len(initial_condition_product) > 0:
                    self.initial_condition_product = initial_condition_product
            else:
                if self.initial_condition and not self.initial_condition_is_homogeneous:
                    self.initial_condition_product = OnlineAffineExpansionStorage(
                        self.Q_ic, self.Q_ic)
                    assert current_stage in ("online", "offline")
                    if current_stage == "online":
                        self.assemble_error_estimation_operators(
                            ("initial_condition", "initial_condition"),
                            "online")
                    elif current_stage == "offline":
                        pass  # Nothing else to be done
                    else:
                        raise ValueError(
                            "Invalid stage in _init_error_estimation_operators()."
                        )

        # Build operators for error estimation
        def build_error_estimation_operators(self, current_stage="offline"):
            ParametrizedReducedDifferentialProblem_DerivedClass.build_error_estimation_operators(
                self, current_stage)
            # Initial condition
            self._build_reduced_initial_condition_error_estimation(
                current_stage)

        def _build_reduced_initial_condition_error_estimation(
                self, current_stage="offline"):
            # Assemble initial condition product error estimation operator
            if len(self.components) > 1:
                for component in self.components:
                    if self.initial_condition[
                            component] and not self.initial_condition_is_homogeneous[
                                component]:
                        self.assemble_error_estimation_operators(
                            ("initial_condition_" + component,
                             "initial_condition_" + component), current_stage)
            else:
                if self.initial_condition and not self.initial_condition_is_homogeneous:
                    self.assemble_error_estimation_operators(
                        ("initial_condition", "initial_condition"),
                        current_stage)

        # Assemble operators for error estimation
        def assemble_error_estimation_operators(self,
                                                term,
                                                current_stage="online"):
            if term[0].startswith("initial_condition") and term[1].startswith(
                    "initial_condition"):
                component0 = term[0].replace("initial_condition",
                                             "").replace("_", "")
                component1 = term[1].replace("initial_condition",
                                             "").replace("_", "")
                assert current_stage in ("online", "offline")
                if current_stage == "online":  # load from file
                    assert (component0 != "") == (component1 != "")
                    if component0 != "":
                        assert component0 in self.components
                        assert component1 in self.components
                        self.initial_condition_product[
                            component0, component1].load(
                                self.folder["error_estimation"],
                                "initial_condition_product_" + component0 +
                                "_" + component1)
                        return self.initial_condition_product[component0,
                                                              component1]
                    else:
                        assert len(self.components) == 1
                        self.initial_condition_product.load(
                            self.folder["error_estimation"],
                            "initial_condition_product")
                        return self.initial_condition_product
                elif current_stage == "offline":
                    inner_product = self.truth_problem._combined_projection_inner_product
                    assert (component0 != "") == (component1 != "")
                    if component0 != "":
                        for q0 in range(self.Q_ic[component0]):
                            for q1 in range(self.Q_ic[component1]):
                                self.initial_condition_product[
                                    component0, component1][q0, q1] = (
                                        transpose(
                                            self.truth_problem.
                                            initial_condition[component0][q0])
                                        * inner_product * self.truth_problem.
                                        initial_condition[component1][q1])
                        self.initial_condition_product[
                            component0, component1].save(
                                self.folder["error_estimation"],
                                "initial_condition_product_" + component0 +
                                "_" + component1)
                        return self.initial_condition_product[component0,
                                                              component1]
                    else:
                        assert len(self.components) == 1
                        for q0 in range(self.Q_ic):
                            for q1 in range(self.Q_ic):
                                self.initial_condition_product[q0, q1] = (
                                    transpose(self.truth_problem.
                                              initial_condition[q0]) *
                                    inner_product *
                                    self.truth_problem.initial_condition[q1])
                        self.initial_condition_product.save(
                            self.folder["error_estimation"],
                            "initial_condition_product")
                        return self.initial_condition_product
                else:
                    raise ValueError(
                        "Invalid stage in assemble_error_estimation_operators()."
                    )
            else:
                return ParametrizedReducedDifferentialProblem_DerivedClass.assemble_error_estimation_operators(
                    self, term, current_stage)

        # Return the error bound for the initial condition
        def get_initial_error_estimate_squared(self):
            self._solution = self._solution_over_time[0]
            N = self._solution.N

            at_least_one_non_homogeneous_initial_condition = False

            addend_0 = 0.
            addend_1_right = OnlineFunction(N)

            if len(self.components) > 1:
                for component in self.components:
                    if self.initial_condition[
                            component] and not self.initial_condition_is_homogeneous[
                                component]:
                        at_least_one_non_homogeneous_initial_condition = True
                        theta_ic_component = self.compute_theta(
                            "initial_condition_" + component)
                        addend_0 += sum(
                            product(
                                theta_ic_component,
                                self.initial_condition_product[component,
                                                               component],
                                theta_ic_component))
                        addend_1_right += sum(
                            product(theta_ic_component,
                                    self.initial_condition[:N]))
            else:
                if self.initial_condition and not self.initial_condition_is_homogeneous:
                    at_least_one_non_homogeneous_initial_condition = True
                    theta_ic = self.compute_theta("initial_condition")
                    addend_0 += sum(
                        product(theta_ic, self.initial_condition_product,
                                theta_ic))
                    addend_1_right += sum(
                        product(theta_ic, self.initial_condition[:N]))

            if at_least_one_non_homogeneous_initial_condition:
                inner_product_N = self._combined_projection_inner_product[:N, :
                                                                          N]
                addend_1_left = self._solution
                addend_2 = transpose(
                    self._solution) * inner_product_N * self._solution
                return addend_0 - 2.0 * (transpose(addend_1_left) *
                                         addend_1_right) + addend_2
            else:
                return 0.
Пример #3
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()
Пример #4
0
class EIMApproximation(ParametrizedProblem):

    # Default initialization of members
    @sync_setters("truth_problem", "set_mu", "mu")
    @sync_setters("truth_problem", "set_mu_range", "mu_range")
    def __init__(self, truth_problem, parametrized_expression, folder_prefix,
                 basis_generation):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized expression
        self.parametrized_expression = parametrized_expression
        self.truth_problem = truth_problem
        assert basis_generation in ("Greedy", "POD")
        self.basis_generation = basis_generation

        # $$ ONLINE DATA STRUCTURES $$ #
        # Online reduced space dimension
        self.N = 0
        # Define additional storage for EIM
        self.interpolation_locations = parametrized_expression.create_interpolation_locations_container(
        )  # interpolation locations selected by the greedy (either a ReducedVertices or ReducedMesh)
        self.interpolation_matrix = OnlineAffineExpansionStorage(
            1)  # interpolation matrix
        # Solution
        self._interpolation_coefficients = None  # OnlineFunction

        # $$ OFFLINE DATA STRUCTURES $$ #
        self.snapshot = None  # will be filled in by Function, Vector or Matrix as appropriate in the EIM preprocessing
        self.snapshot_cache = dict()  # of Function, Vector or Matrix
        # Basis functions container
        self.basis_functions = parametrized_expression.create_basis_container()
        # I/O
        self.folder["basis"] = os.path.join(self.folder_prefix, "basis")
        self.folder["cache"] = os.path.join(self.folder_prefix, "cache")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")
        self.cache_config = config.get("EIM", "cache")

    # Initialize data structures required for the online phase
    def init(self, current_stage="online"):
        assert current_stage in ("online", "offline")
        # Read/Initialize reduced order data structures
        if current_stage == "online":
            self.interpolation_locations.load(self.folder["reduced_operators"],
                                              "interpolation_locations")
            self.interpolation_matrix.load(self.folder["reduced_operators"],
                                           "interpolation_matrix")
            self.basis_functions.load(self.folder["basis"], "basis")
            self.N = len(self.basis_functions)
        elif current_stage == "offline":
            # Nothing to be done
            pass
        else:
            raise ValueError("Invalid stage in init().")

    def evaluate_parametrized_expression(self):
        (cache_key, cache_file) = self._cache_key_and_file()
        if "RAM" in self.cache_config and cache_key in self.snapshot_cache:
            self.snapshot = self.snapshot_cache[cache_key]
        elif "Disk" in self.cache_config and self.import_solution(
                self.folder["cache"], cache_file):
            if "RAM" in self.cache_config:
                self.snapshot_cache[cache_key] = copy(self.snapshot)
        else:
            self.snapshot = evaluate(self.parametrized_expression)
            if "RAM" in self.cache_config:
                self.snapshot_cache[cache_key] = copy(self.snapshot)
            self.export_solution(
                self.folder["cache"], cache_file
            )  # Note that we export to file regardless of config options, because they may change across different runs

    def _cache_key_and_file(self):
        cache_key = self.mu
        cache_file = hashlib.sha1(str(cache_key).encode("utf-8")).hexdigest()
        return (cache_key, cache_file)

    # Perform an online solve.
    def solve(self, N=None):
        if N is None:
            N = self.N

        self._solve(self.parametrized_expression, N)
        return self._interpolation_coefficients

    def _solve(self, rhs_, N=None):
        if N is None:
            N = self.N

        if N > 0:
            self._interpolation_coefficients = OnlineFunction(N)

            # Evaluate the parametrized expression at interpolation locations
            rhs = evaluate(rhs_, self.interpolation_locations[:N])

            (max_abs_rhs, _) = max(abs(rhs))
            if max_abs_rhs == 0.:
                # If the rhs is zero, then we are interpolating the zero function
                # and the default zero coefficients are enough.
                pass
            else:
                # Extract the interpolation matrix
                lhs = self.interpolation_matrix[0][:N, :N]

                # Solve the interpolation problem
                solver = OnlineLinearSolver(lhs,
                                            self._interpolation_coefficients,
                                            rhs)
                solver.solve()
        else:
            self._interpolation_coefficients = None  # OnlineFunction

    # Call online_solve and then convert the result of online solve from OnlineVector to a tuple
    def compute_interpolated_theta(self, N=None):
        interpolated_theta = self.solve(N)
        interpolated_theta_list = list()
        for theta in interpolated_theta:
            interpolated_theta_list.append(theta)
        if N is not None:
            # Make sure to append a 0 coefficient for each basis function
            # which has not been requested
            for n in range(N, self.N):
                interpolated_theta_list.append(0.0)
        return tuple(interpolated_theta_list)

    # Compute the interpolation error and/or its maximum location
    def compute_maximum_interpolation_error(self, N=None):
        if N is None:
            N = self.N

        # Compute the error (difference with the eim approximation)
        if N > 0:
            error = self.snapshot - self.basis_functions[:N] * self._interpolation_coefficients
        else:
            error = copy(
                self.snapshot)  # need a copy because it will be rescaled

        # Get the location of the maximum error
        (maximum_error, maximum_location) = max(abs(error))

        # Return
        return (error, maximum_error, maximum_location)

    def compute_maximum_interpolation_relative_error(self, N=None):
        (absolute_error, maximum_absolute_error,
         maximum_location) = self.compute_maximum_interpolation_error(N)
        (maximum_snapshot_value, _) = max(abs(self.snapshot))
        return (absolute_error / maximum_snapshot_value,
                maximum_absolute_error / maximum_snapshot_value,
                maximum_location)

    # Export solution to file
    def export_solution(self, folder, filename, solution=None):
        if solution is None:
            solution = self.snapshot
        export(solution, folder, filename)

    # Import solution from file
    def import_solution(self, folder, filename, solution=None):
        if solution is None:
            if self.snapshot is None:
                self.snapshot = self.parametrized_expression.create_empty_snapshot(
                )
            solution = self.snapshot
        return import_(solution, folder, filename)
Пример #5
0
class EIMApproximation(ParametrizedProblem):

    # Default initialization of members
    @sync_setters("truth_problem", "set_mu", "mu")
    @sync_setters("truth_problem", "set_mu_range", "mu_range")
    def __init__(self, truth_problem, parametrized_expression, folder_prefix, basis_generation):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized expression
        self.parametrized_expression = parametrized_expression
        self.truth_problem = truth_problem
        assert basis_generation in ("Greedy", "POD")
        self.basis_generation = basis_generation
        
        # $$ ONLINE DATA STRUCTURES $$ #
        # Online reduced space dimension
        self.N = 0
        # Define additional storage for EIM
        self.interpolation_locations = parametrized_expression.create_interpolation_locations_container() # interpolation locations selected by the greedy (either a ReducedVertices or ReducedMesh)
        self.interpolation_matrix = OnlineAffineExpansionStorage(1) # interpolation matrix
        # Solution
        self._interpolation_coefficients = None # OnlineFunction
        
        # $$ OFFLINE DATA STRUCTURES $$ #
        self.snapshot = parametrized_expression.create_empty_snapshot()
        # Basis functions container
        self.basis_functions = parametrized_expression.create_basis_container()
        # I/O
        self.folder["basis"] = os.path.join(self.folder_prefix, "basis")
        self.folder["cache"] = os.path.join(self.folder_prefix, "cache")
        self.folder["reduced_operators"] = os.path.join(self.folder_prefix, "reduced_operators")
        def _snapshot_cache_key_generator(*args, **kwargs):
            assert args == self.mu
            assert len(kwargs) == 0
            return self._cache_key()
        def _snapshot_cache_import(filename):
            snapshot = copy(self.snapshot)
            self.import_solution(self.folder["cache"], filename, snapshot)
            return snapshot
        def _snapshot_cache_export(filename):
            self.export_solution(self.folder["cache"], filename)
        def _snapshot_cache_filename_generator(*args, **kwargs):
            assert args == self.mu
            assert len(kwargs) == 0
            return self._cache_file()
        self._snapshot_cache = Cache(
            "EIM",
            key_generator=_snapshot_cache_key_generator,
            import_=_snapshot_cache_import,
            export=_snapshot_cache_export,
            filename_generator=_snapshot_cache_filename_generator
        )
        
    # Initialize data structures required for the online phase
    def init(self, current_stage="online"):
        assert current_stage in ("online", "offline")
        # Read/Initialize reduced order data structures
        if current_stage == "online":
            self.interpolation_locations.load(self.folder["reduced_operators"], "interpolation_locations")
            self.interpolation_matrix.load(self.folder["reduced_operators"], "interpolation_matrix")
            self.basis_functions.load(self.folder["basis"], "basis")
            self.N = len(self.basis_functions)
        elif current_stage == "offline":
            # Nothing to be done
            pass
        else:
            raise ValueError("Invalid stage in init().")

    def evaluate_parametrized_expression(self):
        try:
            assign(self.snapshot, self._snapshot_cache[self.mu])
        except KeyError:
            self.snapshot = evaluate(self.parametrized_expression)
            self._snapshot_cache[self.mu] = copy(self.snapshot)
        
    def _cache_key(self):
        return self.mu
        
    def _cache_file(self):
        return hashlib.sha1(str(self._cache_key()).encode("utf-8")).hexdigest()
        
    # Perform an online solve.
    def solve(self, N=None):
        if N is None:
            N = self.N
        
        self._solve(self.parametrized_expression, N)
        return self._interpolation_coefficients
        
    def _solve(self, rhs_, N=None):
        if N is None:
            N = self.N
            
        if N > 0:
            self._interpolation_coefficients = OnlineFunction(N)
                
            # Evaluate the parametrized expression at interpolation locations
            rhs = evaluate(rhs_, self.interpolation_locations[:N])
            
            (max_abs_rhs, _) = max(abs(rhs))
            if max_abs_rhs == 0.:
                # If the rhs is zero, then we are interpolating the zero function
                # and the default zero coefficients are enough.
                pass
            else:
                # Extract the interpolation matrix
                lhs = self.interpolation_matrix[0][:N, :N]
                
                # Solve the interpolation problem
                solver = OnlineLinearSolver(lhs, self._interpolation_coefficients, rhs)
                solver.solve()
        else:
            self._interpolation_coefficients = None # OnlineFunction
        
    # Call online_solve and then convert the result of online solve from OnlineVector to a tuple
    def compute_interpolated_theta(self, N=None):
        interpolated_theta = self.solve(N)
        interpolated_theta_list = list()
        for theta in interpolated_theta:
            interpolated_theta_list.append(theta)
        if N is not None:
            # Make sure to append a 0 coefficient for each basis function
            # which has not been requested
            for n in range(N, self.N):
                interpolated_theta_list.append(0.0)
        return tuple(interpolated_theta_list)

    # Compute the interpolation error and/or its maximum location
    def compute_maximum_interpolation_error(self, N=None):
        if N is None:
            N = self.N
        
        # Compute the error (difference with the eim approximation)
        if N > 0:
            error = self.snapshot - self.basis_functions[:N]*self._interpolation_coefficients
        else:
            error = copy(self.snapshot) # need a copy because it will be rescaled
        
        # Get the location of the maximum error
        (maximum_error, maximum_location) = max(abs(error))
        
        # Return
        return (error, maximum_error, maximum_location)
        
    def compute_maximum_interpolation_relative_error(self, N=None):
        (absolute_error, maximum_absolute_error, maximum_location) = self.compute_maximum_interpolation_error(N)
        (maximum_snapshot_value, _) = max(abs(self.snapshot))
        if maximum_snapshot_value != 0.:
            return (absolute_error/maximum_snapshot_value, maximum_absolute_error/maximum_snapshot_value, maximum_location)
        else:
            if maximum_absolute_error == 0.:
                return (absolute_error, maximum_absolute_error, maximum_location) # the first two arguments are a zero expression and zero scalar
            else:
                return (None, float("NaN"), maximum_location) # the first argument should be a NaN expression
                
    # Export solution to file
    def export_solution(self, folder=None, filename=None, solution=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "snapshot"
        if solution is None:
            solution = self.snapshot
        export(solution, folder, filename)
        
    # Import solution from file
    def import_solution(self, folder=None, filename=None, solution=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "snapshot"
        if solution is None:
            solution = self.snapshot
        import_(solution, folder, filename)