Beispiel #1
0
 def init(self, current_stage="online"):
     assert current_stage in ("online", "offline")
     # Init truth problem, to setup stability_factor_{left,right}_hand_matrix operators
     self.truth_problem.init()
     # Init exact stability factor computations
     self.stability_factor_calculator.init()
     # Read/Initialize reduced order data structures
     if current_stage == "online":
         self.bounding_box_min.load(self.folder["reduced_operators"],
                                    "bounding_box_min")
         self.bounding_box_max.load(self.folder["reduced_operators"],
                                    "bounding_box_max")
         self.training_set.load(self.folder["reduced_operators"],
                                "training_set")
         self.greedy_selected_parameters.load(
             self.folder["reduced_operators"], "greedy_selected_parameters")
         self.upper_bound_vectors.load(self.folder["reduced_operators"],
                                       "upper_bound_vectors")
         # Set the value of N
         self.N = len(self.greedy_selected_parameters)
     elif current_stage == "offline":
         # Properly resize structures related to operator
         Q = self.truth_problem.Q["stability_factor_left_hand_matrix"]
         self.bounding_box_min = BoundingBoxSideList(Q)
         self.bounding_box_max = BoundingBoxSideList(Q)
         # Save the training set, which was passed by the reduction method,
         # in order to use it online
         assert self.training_set is not None
         self.training_set.save(self.folder["reduced_operators"],
                                "training_set")
         # Properly initialize structures related to greedy selected parameters
         assert len(self.greedy_selected_parameters) == 0
     else:
         raise ValueError("Invalid stage in init().")
Beispiel #2
0
 def init(self, current_stage="online"):
     assert current_stage in ("online", "offline")
     # Read/Initialize reduced order data structures
     if current_stage == "online":
         self.B_min.load(self.folder["reduced_operators"], "B_min")
         self.B_max.load(self.folder["reduced_operators"], "B_max")
         self.training_set.load(self.folder["reduced_operators"],
                                "training_set")
         self.greedy_selected_parameters.load(
             self.folder["reduced_operators"], "greedy_selected_parameters")
         self.UB_vectors.load(self.folder["reduced_operators"],
                              "UB_vectors")
         # Set the value of N
         self.N = len(self.greedy_selected_parameters)
     elif current_stage == "offline":
         self.truth_problem.init()
         # Properly resize structures related to operator
         Q = self.truth_problem.Q["a"]
         self.B_min = BoundingBoxSideList(Q)
         self.B_max = BoundingBoxSideList(Q)
         # Save the training set, which was passed by the reduction method,
         # in order to use it online
         assert self.training_set is not None
         self.training_set.save(self.folder["reduced_operators"],
                                "training_set")
         # Properly initialize structures related to greedy selected parameters
         assert len(self.greedy_selected_parameters) is 0
         # Init exact coercivity constant computations
         self.exact_coercivity_constant_calculator.init()
     else:
         raise ValueError("Invalid stage in init().")
Beispiel #3
0
    def __init__(self, truth_problem, folder_prefix, **kwargs):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized problem object and the bc list
        self.truth_problem = truth_problem

        # Define additional storage for SCM
        self.B_min = BoundingBoxSideList(
        )  # minimum values of the bounding box mathcal{B}. Vector of size Q
        self.B_max = BoundingBoxSideList(
        )  # maximum values of the bounding box mathcal{B}. Vector of size Q
        self.training_set = None  # SCM algorithm needs the training set also in the online stage
        self.greedy_selected_parameters = GreedySelectedParametersList(
        )  # list storing the parameters selected during the training phase
        self.greedy_selected_parameters_complement = dict(
        )  # dict, over N, of list storing the complement of parameters selected during the training phase
        self.UB_vectors = UpperBoundsList(
        )  # list of Q-dimensional vectors storing the infimizing elements at the greedily selected parameters
        self.N = 0
        self.M_e = kwargs[
            "M_e"]  # integer denoting the number of constraints based on the exact eigenvalues, or None
        self.M_p = kwargs[
            "M_p"]  # integer denoting the number of constraints based on the previous lower bounds, or None

        # I/O
        self.folder["cache"] = os.path.join(self.folder_prefix,
                                            "reduced_cache")
        self.cache_config = config.get("SCM", "cache")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")

        # Coercivity constant eigen problem
        self.exact_coercivity_constant_calculator = ParametrizedCoercivityConstantEigenProblem(
            truth_problem, "a", True, "smallest",
            kwargs["coercivity_eigensolver_parameters"], self.folder_prefix)

        # Store here input parameters provided by the user that are needed by the reduction method
        self._input_storage_for_SCM_reduction = dict()
        self._input_storage_for_SCM_reduction[
            "bounding_box_minimum_eigensolver_parameters"] = kwargs[
                "bounding_box_minimum_eigensolver_parameters"]
        self._input_storage_for_SCM_reduction[
            "bounding_box_maximum_eigensolver_parameters"] = kwargs[
                "bounding_box_maximum_eigensolver_parameters"]

        # Avoid useless linear programming solves
        self._alpha_LB = 0.
        self._alpha_LB_cache = dict()
        self._alpha_UB = 0.
        self._alpha_UB_cache = dict()
Beispiel #4
0
    def __init__(self, truth_problem, folder_prefix):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized problem object and the bc list
        self.truth_problem = truth_problem

        # Define additional storage for SCM
        self.bounding_box_min = BoundingBoxSideList(
        )  # minimum values of the bounding box. Vector of size Q
        self.bounding_box_max = BoundingBoxSideList(
        )  # maximum values of the bounding box. Vector of size Q
        self.training_set = None  # SCM algorithm needs the training set also in the online stage
        # greedy_selected_parameters: list storing the parameters selected during the training phase
        self.greedy_selected_parameters = GreedySelectedParametersList()
        # greedy_selected_parameters_complement: dict, over N, of list storing the complement of parameters
        # selected during the training phase
        self.greedy_selected_parameters_complement = dict()
        # upper_bound_vectors: list of Q-dimensional vectors storing the infimizing elements at the greedily
        # selected parameters
        self.upper_bound_vectors = UpperBoundsList()
        self.N = 0

        # Storage for online computations
        self._stability_factor_lower_bound = 0.
        self._stability_factor_upper_bound = 0.

        # I/O
        self.folder["cache"] = os.path.join(self.folder_prefix,
                                            "reduced_cache")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")

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

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

        def _stability_factor_lower_bound_cache_import(filename):
            self.import_stability_factor_lower_bound(self.folder["cache"],
                                                     filename)
            return self._stability_factor_lower_bound

        def _stability_factor_lower_bound_cache_export(filename):
            self.export_stability_factor_lower_bound(self.folder["cache"],
                                                     filename)

        self._stability_factor_lower_bound_cache = Cache(
            "SCM",
            key_generator=_stability_factor_cache_key_generator,
            import_=_stability_factor_lower_bound_cache_import,
            export=_stability_factor_lower_bound_cache_export,
            filename_generator=_stability_factor_cache_filename_generator)

        def _stability_factor_upper_bound_cache_import(filename):
            self.import_stability_factor_upper_bound(self.folder["cache"],
                                                     filename)
            return self._stability_factor_upper_bound

        def _stability_factor_upper_bound_cache_export(filename):
            self.export_stability_factor_upper_bound(self.folder["cache"],
                                                     filename)

        self._stability_factor_upper_bound_cache = Cache(
            "SCM",
            key_generator=_stability_factor_cache_key_generator,
            import_=_stability_factor_upper_bound_cache_import,
            export=_stability_factor_upper_bound_cache_export,
            filename_generator=_stability_factor_cache_filename_generator)

        # Stability factor eigen problem
        self.stability_factor_calculator = ParametrizedStabilityFactorEigenProblem(
            self.truth_problem, "smallest",
            self.truth_problem._eigen_solver_parameters["stability_factor"],
            self.folder_prefix)
Beispiel #5
0
class SCMApproximation(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, folder_prefix):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized problem object and the bc list
        self.truth_problem = truth_problem

        # Define additional storage for SCM
        self.bounding_box_min = BoundingBoxSideList(
        )  # minimum values of the bounding box. Vector of size Q
        self.bounding_box_max = BoundingBoxSideList(
        )  # maximum values of the bounding box. Vector of size Q
        self.training_set = None  # SCM algorithm needs the training set also in the online stage
        # greedy_selected_parameters: list storing the parameters selected during the training phase
        self.greedy_selected_parameters = GreedySelectedParametersList()
        # greedy_selected_parameters_complement: dict, over N, of list storing the complement of parameters
        # selected during the training phase
        self.greedy_selected_parameters_complement = dict()
        # upper_bound_vectors: list of Q-dimensional vectors storing the infimizing elements at the greedily
        # selected parameters
        self.upper_bound_vectors = UpperBoundsList()
        self.N = 0

        # Storage for online computations
        self._stability_factor_lower_bound = 0.
        self._stability_factor_upper_bound = 0.

        # I/O
        self.folder["cache"] = os.path.join(self.folder_prefix,
                                            "reduced_cache")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")

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

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

        def _stability_factor_lower_bound_cache_import(filename):
            self.import_stability_factor_lower_bound(self.folder["cache"],
                                                     filename)
            return self._stability_factor_lower_bound

        def _stability_factor_lower_bound_cache_export(filename):
            self.export_stability_factor_lower_bound(self.folder["cache"],
                                                     filename)

        self._stability_factor_lower_bound_cache = Cache(
            "SCM",
            key_generator=_stability_factor_cache_key_generator,
            import_=_stability_factor_lower_bound_cache_import,
            export=_stability_factor_lower_bound_cache_export,
            filename_generator=_stability_factor_cache_filename_generator)

        def _stability_factor_upper_bound_cache_import(filename):
            self.import_stability_factor_upper_bound(self.folder["cache"],
                                                     filename)
            return self._stability_factor_upper_bound

        def _stability_factor_upper_bound_cache_export(filename):
            self.export_stability_factor_upper_bound(self.folder["cache"],
                                                     filename)

        self._stability_factor_upper_bound_cache = Cache(
            "SCM",
            key_generator=_stability_factor_cache_key_generator,
            import_=_stability_factor_upper_bound_cache_import,
            export=_stability_factor_upper_bound_cache_export,
            filename_generator=_stability_factor_cache_filename_generator)

        # Stability factor eigen problem
        self.stability_factor_calculator = ParametrizedStabilityFactorEigenProblem(
            self.truth_problem, "smallest",
            self.truth_problem._eigen_solver_parameters["stability_factor"],
            self.folder_prefix)

    # Initialize data structures required for the online phase
    def init(self, current_stage="online"):
        assert current_stage in ("online", "offline")
        # Init truth problem, to setup stability_factor_{left,right}_hand_matrix operators
        self.truth_problem.init()
        # Init exact stability factor computations
        self.stability_factor_calculator.init()
        # Read/Initialize reduced order data structures
        if current_stage == "online":
            self.bounding_box_min.load(self.folder["reduced_operators"],
                                       "bounding_box_min")
            self.bounding_box_max.load(self.folder["reduced_operators"],
                                       "bounding_box_max")
            self.training_set.load(self.folder["reduced_operators"],
                                   "training_set")
            self.greedy_selected_parameters.load(
                self.folder["reduced_operators"], "greedy_selected_parameters")
            self.upper_bound_vectors.load(self.folder["reduced_operators"],
                                          "upper_bound_vectors")
            # Set the value of N
            self.N = len(self.greedy_selected_parameters)
        elif current_stage == "offline":
            # Properly resize structures related to operator
            Q = self.truth_problem.Q["stability_factor_left_hand_matrix"]
            self.bounding_box_min = BoundingBoxSideList(Q)
            self.bounding_box_max = BoundingBoxSideList(Q)
            # Save the training set, which was passed by the reduction method,
            # in order to use it online
            assert self.training_set is not None
            self.training_set.save(self.folder["reduced_operators"],
                                   "training_set")
            # Properly initialize structures related to greedy selected parameters
            assert len(self.greedy_selected_parameters) == 0
        else:
            raise ValueError("Invalid stage in init().")

    def evaluate_stability_factor(self):
        return self.stability_factor_calculator.solve()

    # Get a lower bound for the stability factor
    def get_stability_factor_lower_bound(self, N=None):
        if N is None:
            N = self.N
        try:
            self._stability_factor_lower_bound = self._stability_factor_lower_bound_cache[
                self.mu, N]
        except KeyError:
            self._get_stability_factor_lower_bound(N)
            self._stability_factor_lower_bound_cache[
                self.mu, N] = self._stability_factor_lower_bound
        return self._stability_factor_lower_bound

    def _get_stability_factor_lower_bound(self, N):
        assert N <= len(self.greedy_selected_parameters)
        Q = self.truth_problem.Q["stability_factor_left_hand_matrix"]
        M_e = N
        M_p = min(
            N,
            len(self.training_set) - len(self.greedy_selected_parameters))

        # 1. Constrain the Q variables to be in the bounding box
        bounds = list()  # of Q pairs
        for q in range(Q):
            assert self.bounding_box_min[q] <= self.bounding_box_max[q]
            bounds.append((self.bounding_box_min[q], self.bounding_box_max[q]))

        # 2. Add three different sets of constraints.
        #    Our constrains are of the form
        #       a^T * x >= b
        constraints_matrix = Matrix(M_e + M_p + 1, Q)
        constraints_vector = Vector(M_e + M_p + 1)

        # 2a. Add constraints: a constraint is added for the closest samples to mu among the selected parameters
        mu_bak = self.mu
        closest_selected_parameters = self._closest_selected_parameters(
            M_e, N, self.mu)
        for (j, omega) in enumerate(closest_selected_parameters):
            # Overwrite parameter values
            self.set_mu(omega)

            # Compute theta
            current_theta = self.truth_problem.compute_theta(
                "stability_factor_left_hand_matrix")

            # Assemble the LHS of the constraint
            for q in range(Q):
                constraints_matrix[j, q] = current_theta[q]

            # Assemble the RHS of the constraint: note that computations for this call may be already cached
            (constraints_vector[j], _) = self.evaluate_stability_factor()
        self.set_mu(mu_bak)

        # 2b. Add constraints: also constrain the closest point in the complement of selected parameters,
        #                      with RHS depending on previously computed lower bounds
        mu_bak = self.mu
        closest_selected_parameters_complement = self._closest_unselected_parameters(
            M_p, N, self.mu)
        for (j, nu) in enumerate(closest_selected_parameters_complement):
            # Overwrite parameter values
            self.set_mu(nu)

            # Compute theta
            current_theta = self.truth_problem.compute_theta(
                "stability_factor_left_hand_matrix")

            # Assemble the LHS of the constraint
            for q in range(Q):
                constraints_matrix[M_e + j, q] = current_theta[q]

            # Assemble the RHS of the constraint: note that computations for this call may be already cached
            if N > 1:
                constraints_vector[
                    M_e + j] = self.get_stability_factor_lower_bound(N - 1)
            else:
                constraints_vector[M_e + j] = 0.
        self.set_mu(mu_bak)

        # 2c. Add constraints: also constrain the stability factor for mu to be positive
        # Compute theta
        current_theta = self.truth_problem.compute_theta(
            "stability_factor_left_hand_matrix")

        # Assemble the LHS of the constraint
        for q in range(Q):
            constraints_matrix[M_e + M_p, q] = current_theta[q]

        # Assemble the RHS of the constraint
        constraints_vector[M_e + M_p] = 0.

        # 3. Add cost function coefficients
        cost = Vector(Q)
        for q in range(Q):
            cost[q] = current_theta[q]

        # 4. Solve the linear programming problem
        linear_program = LinearProgramSolver(cost, constraints_matrix,
                                             constraints_vector, bounds)
        try:
            stability_factor_lower_bound = linear_program.solve()
        except LinearProgramSolverError:
            print("SCM warning at mu = " + str(self.mu) +
                  ": error occured while solving linear program.")
            print(
                "Please consider switching to a different solver. A truth eigensolve will be performed."
            )

            (stability_factor_lower_bound,
             _) = self.evaluate_stability_factor()

        self._stability_factor_lower_bound = stability_factor_lower_bound

    # Get an upper bound for the stability factor
    def get_stability_factor_upper_bound(self, N=None):
        if N is None:
            N = self.N
        try:
            self._stability_factor_upper_bound = self._stability_factor_upper_bound_cache[
                self.mu, N]
        except KeyError:
            self._get_stability_factor_upper_bound(N)
            self._stability_factor_upper_bound_cache[
                self.mu, N] = self._stability_factor_upper_bound
        return self._stability_factor_upper_bound

    def _get_stability_factor_upper_bound(self, N):
        Q = self.truth_problem.Q["stability_factor_left_hand_matrix"]
        upper_bound_vectors = self.upper_bound_vectors

        stability_factor_upper_bound = None
        current_theta = self.truth_problem.compute_theta(
            "stability_factor_left_hand_matrix")

        for j in range(N):
            upper_bound_vector = upper_bound_vectors[j]

            # Compute the cost function for fixed omega
            obj = 0.
            for q in range(Q):
                obj += upper_bound_vector[q] * current_theta[q]

            if stability_factor_upper_bound is None or obj < stability_factor_upper_bound:
                stability_factor_upper_bound = obj

        assert stability_factor_upper_bound is not None
        self._stability_factor_upper_bound = stability_factor_upper_bound

    def _cache_key(self, N):
        return (self.mu, N)

    def _cache_file(self, N):
        return hashlib.sha1(str(
            self._cache_key(N)).encode("utf-8")).hexdigest()

    def _closest_selected_parameters(self, M, N, mu):
        return self.greedy_selected_parameters[:N].closest(M, mu)

    def _closest_unselected_parameters(self, M, N, mu):
        if N not in self.greedy_selected_parameters_complement:
            self.greedy_selected_parameters_complement[
                N] = self.training_set.diff(
                    self.greedy_selected_parameters[:N])
        return self.greedy_selected_parameters_complement[N].closest(M, mu)

    def export_stability_factor_lower_bound(self, folder=None, filename=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "stability_factor"
        export([self._stability_factor_lower_bound], folder,
               filename + "_lower_bound")

    def export_stability_factor_upper_bound(self, folder=None, filename=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "stability_factor"
        export([self._stability_factor_upper_bound], folder,
               filename + "_upper_bound")

    def import_stability_factor_lower_bound(self, folder=None, filename=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "stability_factor"
        stability_factor_lower_bound_storage = [0.]
        import_(stability_factor_lower_bound_storage, folder,
                filename + "_lower_bound")
        assert len(stability_factor_lower_bound_storage) == 1
        self._stability_factor_lower_bound = stability_factor_lower_bound_storage[
            0]

    def import_stability_factor_upper_bound(self, folder=None, filename=None):
        if folder is None:
            folder = self.folder_prefix
        if filename is None:
            filename = "stability_factor"
        stability_factor_upper_bound_storage = [0.]
        import_(stability_factor_upper_bound_storage, folder,
                filename + "_upper_bound")
        assert len(stability_factor_upper_bound_storage) == 1
        self._stability_factor_upper_bound = stability_factor_upper_bound_storage[
            0]
Beispiel #6
0
class SCMApproximation(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, folder_prefix, **kwargs):
        # Call the parent initialization
        ParametrizedProblem.__init__(self, folder_prefix)
        # Store the parametrized problem object and the bc list
        self.truth_problem = truth_problem

        # Define additional storage for SCM
        self.B_min = BoundingBoxSideList(
        )  # minimum values of the bounding box mathcal{B}. Vector of size Q
        self.B_max = BoundingBoxSideList(
        )  # maximum values of the bounding box mathcal{B}. Vector of size Q
        self.training_set = None  # SCM algorithm needs the training set also in the online stage
        self.greedy_selected_parameters = GreedySelectedParametersList(
        )  # list storing the parameters selected during the training phase
        self.greedy_selected_parameters_complement = dict(
        )  # dict, over N, of list storing the complement of parameters selected during the training phase
        self.UB_vectors = UpperBoundsList(
        )  # list of Q-dimensional vectors storing the infimizing elements at the greedily selected parameters
        self.N = 0
        self.M_e = kwargs[
            "M_e"]  # integer denoting the number of constraints based on the exact eigenvalues, or None
        self.M_p = kwargs[
            "M_p"]  # integer denoting the number of constraints based on the previous lower bounds, or None

        # I/O
        self.folder["cache"] = os.path.join(self.folder_prefix,
                                            "reduced_cache")
        self.cache_config = config.get("SCM", "cache")
        self.folder["reduced_operators"] = os.path.join(
            self.folder_prefix, "reduced_operators")

        # Coercivity constant eigen problem
        self.exact_coercivity_constant_calculator = ParametrizedCoercivityConstantEigenProblem(
            truth_problem, "a", True, "smallest",
            kwargs["coercivity_eigensolver_parameters"], self.folder_prefix)

        # Store here input parameters provided by the user that are needed by the reduction method
        self._input_storage_for_SCM_reduction = dict()
        self._input_storage_for_SCM_reduction[
            "bounding_box_minimum_eigensolver_parameters"] = kwargs[
                "bounding_box_minimum_eigensolver_parameters"]
        self._input_storage_for_SCM_reduction[
            "bounding_box_maximum_eigensolver_parameters"] = kwargs[
                "bounding_box_maximum_eigensolver_parameters"]

        # Avoid useless linear programming solves
        self._alpha_LB = 0.
        self._alpha_LB_cache = dict()
        self._alpha_UB = 0.
        self._alpha_UB_cache = dict()

    # 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.B_min.load(self.folder["reduced_operators"], "B_min")
            self.B_max.load(self.folder["reduced_operators"], "B_max")
            self.training_set.load(self.folder["reduced_operators"],
                                   "training_set")
            self.greedy_selected_parameters.load(
                self.folder["reduced_operators"], "greedy_selected_parameters")
            self.UB_vectors.load(self.folder["reduced_operators"],
                                 "UB_vectors")
            # Set the value of N
            self.N = len(self.greedy_selected_parameters)
        elif current_stage == "offline":
            self.truth_problem.init()
            # Properly resize structures related to operator
            Q = self.truth_problem.Q["a"]
            self.B_min = BoundingBoxSideList(Q)
            self.B_max = BoundingBoxSideList(Q)
            # Save the training set, which was passed by the reduction method,
            # in order to use it online
            assert self.training_set is not None
            self.training_set.save(self.folder["reduced_operators"],
                                   "training_set")
            # Properly initialize structures related to greedy selected parameters
            assert len(self.greedy_selected_parameters) is 0
            # Init exact coercivity constant computations
            self.exact_coercivity_constant_calculator.init()
        else:
            raise ValueError("Invalid stage in init().")

    def evaluate_stability_factor(self):
        return self.exact_coercivity_constant_calculator.solve()

    # Get a lower bound for alpha
    def get_stability_factor_lower_bound(self, N=None):
        if N is None:
            N = self.N
        assert N <= len(self.greedy_selected_parameters)
        (cache_key, cache_file) = self._cache_key_and_file(N)
        if "RAM" in self.cache_config and cache_key in self._alpha_LB_cache:
            log(PROGRESS, "Loading stability factor lower bound from cache")
            self._alpha_LB = self._alpha_LB_cache[cache_key]
        elif "Disk" in self.cache_config and self.import_stability_factor_lower_bound(
                self.folder["cache"], cache_file):
            log(PROGRESS, "Loading stability factor lower bound from file")
            if "RAM" in self.cache_config:
                self._alpha_LB_cache[cache_key] = self._alpha_LB
        else:
            log(PROGRESS,
                "Solving stability factor lower bound reduced problem")
            Q = self.truth_problem.Q["a"]
            M_e = min(self.M_e if self.M_e is not None else N, N,
                      len(self.greedy_selected_parameters))
            M_p = min(
                self.M_p if self.M_p is not None else N, N,
                len(self.training_set) - len(self.greedy_selected_parameters))

            # 1. Constrain the Q variables to be in the bounding box
            bounds = list()  # of Q pairs
            for q in range(Q):
                assert self.B_min[q] <= self.B_max[q]
                bounds.append((self.B_min[q], self.B_max[q]))

            # 2. Add three different sets of constraints.
            #    Our constrains are of the form
            #       a^T * x >= b
            constraints_matrix = Matrix(M_e + M_p + 1, Q)
            constraints_vector = Vector(M_e + M_p + 1)

            # 2a. Add constraints: a constraint is added for the closest samples to mu among the selected parameters
            mu_bak = self.mu
            closest_selected_parameters = self._closest_selected_parameters(
                M_e, N, self.mu)
            for (j, omega) in enumerate(closest_selected_parameters):
                # Overwrite parameter values
                self.set_mu(omega)

                # Compute theta
                current_theta_a = self.truth_problem.compute_theta("a")

                # Assemble the LHS of the constraint
                for q in range(Q):
                    constraints_matrix[j, q] = current_theta_a[q]

                # Assemble the RHS of the constraint
                (constraints_vector[j], _) = self.evaluate_stability_factor(
                )  # note that computations for this call may be already cached
            self.set_mu(mu_bak)

            # 2b. Add constraints: also constrain the closest point in the complement of selected parameters,
            #                      with RHS depending on previously computed lower bounds
            mu_bak = self.mu
            closest_selected_parameters_complement = self._closest_unselected_parameters(
                M_p, N, self.mu)
            for (j, nu) in enumerate(closest_selected_parameters_complement):
                # Overwrite parameter values
                self.set_mu(nu)

                # Compute theta
                current_theta_a = self.truth_problem.compute_theta("a")

                # Assemble the LHS of the constraint
                for q in range(Q):
                    constraints_matrix[M_e + j, q] = current_theta_a[q]

                # Assemble the RHS of the constraint
                if N > 1:
                    constraints_vector[
                        M_e + j] = self.get_stability_factor_lower_bound(
                            N - 1
                        )  # note that computations for this call may be already cached
                else:
                    constraints_vector[M_e + j] = 0.
            self.set_mu(mu_bak)

            # 2c. Add constraints: also constrain the coercivity constant for mu to be positive
            # Compute theta
            current_theta_a = self.truth_problem.compute_theta("a")

            # Assemble the LHS of the constraint
            for q in range(Q):
                constraints_matrix[M_e + M_p, q] = current_theta_a[q]

            # Assemble the RHS of the constraint
            constraints_vector[M_e + M_p] = 0.

            # 3. Add cost function coefficients
            cost = Vector(Q)
            for q in range(Q):
                cost[q] = current_theta_a[q]

            # 4. Solve the linear programming problem
            linear_program = LinearProgramSolver(cost, constraints_matrix,
                                                 constraints_vector, bounds)
            try:
                alpha_LB = linear_program.solve()
            except LinearProgramSolverError:
                print("SCM warning at mu = " + str(self.mu) +
                      ": error occured while solving linear program.")
                print(
                    "Please consider switching to a different solver. A truth eigensolve will be performed."
                )

                (alpha_LB, _) = self.evaluate_stability_factor()

            self._alpha_LB = alpha_LB
            if "RAM" in self.cache_config:
                self._alpha_LB_cache[cache_key] = alpha_LB
            self.export_stability_factor_lower_bound(
                self.folder["cache"], cache_file
            )  # Note that we export to file regardless of config options, because they may change across different runs
        return self._alpha_LB

    # Get an upper bound for alpha
    def get_stability_factor_upper_bound(self, N=None):
        if N is None:
            N = self.N
        (cache_key, cache_file) = self._cache_key_and_file(N)
        if "RAM" in self.cache_config and cache_key in self._alpha_UB_cache:
            log(PROGRESS, "Loading stability factor upper bound from cache")
            self._alpha_UB = self._alpha_UB_cache[cache_key]
        elif "Disk" in self.cache_config and self.import_stability_factor_upper_bound(
                self.folder["cache"], cache_file):
            log(PROGRESS, "Loading stability factor upper bound from file")
            if "RAM" in self.cache_config:
                self._alpha_UB_cache[cache_key] = self._alpha_UB
        else:
            log(PROGRESS,
                "Solving stability factor upper bound reduced problem")
            Q = self.truth_problem.Q["a"]
            UB_vectors = self.UB_vectors

            alpha_UB = None
            current_theta_a = self.truth_problem.compute_theta("a")

            for j in range(N):
                UB_vector = UB_vectors[j]

                # Compute the cost function for fixed omega
                obj = 0.
                for q in range(Q):
                    obj += UB_vector[q] * current_theta_a[q]

                if alpha_UB is None or obj < alpha_UB:
                    alpha_UB = obj

            assert alpha_UB is not None
            self._alpha_UB = alpha_UB
            if "RAM" in self.cache_config:
                self._alpha_UB_cache[cache_key] = alpha_UB
            self.export_stability_factor_upper_bound(
                self.folder["cache"], cache_file
            )  # Note that we export to file regardless of config options, because they may change across different runs
        return self._alpha_UB

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

    def _closest_selected_parameters(self, M, N, mu):
        return self.greedy_selected_parameters[:N].closest(M, mu)

    def _closest_unselected_parameters(self, M, N, mu):
        if N not in self.greedy_selected_parameters_complement:
            self.greedy_selected_parameters_complement[
                N] = self.training_set.diff(
                    self.greedy_selected_parameters[:N])
        return self.greedy_selected_parameters_complement[N].closest(M, mu)

    def export_stability_factor_lower_bound(self, folder, filename):
        export([self._alpha_LB], folder, filename + "_LB")

    def export_stability_factor_upper_bound(self, folder, filename):
        export([self._alpha_UB], folder, filename + "_UB")

    def import_stability_factor_lower_bound(self, folder, filename):
        eigenvalue_storage = [0.]
        import_successful = import_(eigenvalue_storage, folder,
                                    filename + "_LB")
        if import_successful:
            assert len(eigenvalue_storage) == 1
            self._alpha_LB = eigenvalue_storage[0]
        return import_successful

    def import_stability_factor_upper_bound(self, folder, filename):
        eigenvalue_storage = [0.]
        import_successful = import_(eigenvalue_storage, folder,
                                    filename + "_UB")
        if import_successful:
            assert len(eigenvalue_storage) == 1
            self._alpha_UB = eigenvalue_storage[0]
        return import_successful