Пример #1
0
    def preference_percentages(self, val: np.ndarray):
        """Set the percentages to descripe how to improve each objective.

        Args:
            val (np.ndarray): A 1D-vector containing percentages corresponding
            to each objective.

        Raises:
            InteractiveMethod: The lenght of the prcentages vector does not
            match the number of objectives in the problem.

        """
        if len(val) != self.problem.n_of_objectives:
            msg = ("The number of given percentages '{}' does not match the "
                   "number of objectives '{}'").format(
                       len(val), self.problem.n_of_objectives)
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        elif np.sum(val) != 100:
            msg = ("The sum of the percentages must be 100. Current sum"
                   " is {}.").format(np.sum(val))
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        self.__preference_percentages = val
Пример #2
0
    def preference_index_set(self, val: np.ndarray):
        """Set the indexes to rank each of the objectives in order of
        importance.

        Args:
            val (np.ndarray): A 1D-vector containing the preference index
            corresponding to each obejctive.

        Raise:
            InteractiveMethodError: The length of the index vector does not
            match the number of objectives in the problem.

        """
        if len(val) != self.problem.n_of_objectives:
            msg = ("The number of indices '{}' does not match the number "
                   "of objectives '{}'").format(len(val),
                                                self.problem.n_of_objectives)
            logger.debug(msg)
            raise InteractiveMethodError(msg)
        elif not (1 <= max(val) <= self.problem.n_of_objectives):
            msg = ("The minimum index of importance must be greater or equal "
                   "to 1 and the maximum index of improtance must be less "
                   "than or equal to the number of objectives in the "
                   "problem, which is {}. Check the indices {}").format(
                       self.problem.n_of_objectives, val)
            logger.debug(msg)
            raise InteractiveMethodError(msg)
        self.__preference_index_set = val
Пример #3
0
    def ith(self, val: int):
        """Set the number of remaining iterations. Should be less than the current
        remaining iterations

        Args:
            val (int): New number of iterations to carry out.

        Raises:
            InteractiveMethodError: val is either negative or greater than the
            current number of remaining iterations.

        """
        if val < 0:
            msg = ("The given number of iterations left "
                   "should be positive. Given iterations '{}'".format(
                       str(val)))
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        elif val > self.__ith:
            msg = ("The given number of iterations '{}' left should be less "
                   "than the current number of iterations left '{}'").format(
                       val, self.__ith)
            logger.debug(msg)
            raise InteractiveMethodError(msg)
        self.__ith = val
Пример #4
0
 def search_between_points(self, val: Tuple[np.ndarray, np.ndarray]):
     if len(val) != 2:
         msg = (
             "To generate intermediate points, two points must be "
             "specified. Number of given points were {}"
         ).format(len(val))
         logger.error(msg)
         raise InteractiveMethodError(msg)
     self.__search_between_points = val
Пример #5
0
    def n_intermediate_solutions(self, val: int):
        if val < 1:
            msg = (
                "Number of intermediate points to be generated must be "
                "positive definitive. Given number of points {}."
            ).format(val)
            logger.error(msg)
            raise InteractiveMethodError(msg)

        self.__n_intermediate_solutions = val
Пример #6
0
    def n_points(self, val: int):
        """The number of points to be presented to the DM during each iteration.

        Args:
            val (int): Number of points to be shown.

        Raises:
            InteractiveMethodError: The number of points is non-positive.

        """
        if val < 1:
            msg = "The number of points shown must be greater than zero."
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        self.__n_points = val
Пример #7
0
    def n_iters(self, val: int):
        """Set the total number of iterations to be carried out.

        Args:
            val (int): The number of iterations.

        Raises:
            InteractiveMethodError: The number of iterations is non-positive.

        """
        if val < 1:
            msg = "Number of iterations must be greater than zero."
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        self.__n_iters = val
Пример #8
0
    def itn(self, val: int):
        """Set the number of total iterations to be carried.

        Args:
            val (int): The total number of iterations. Must be positive.

        Raises:
            InteractiveMethodError: val is not positive.

        """
        if val < 0:
            msg = ("The number of total iterations "
                   "should be positive. Given iterations '{}'".format(
                       str(val)))
            logger.debug(msg)
            raise InteractiveMethodError(msg)
        self.__itn = val
Пример #9
0
    def ideal(self, val: np.ndarray):
        """Set the ideal point.

        Args:
            val (np.ndarray): The ideal point.

        Raises:
            InteractiveMethodError: The ideal point is of the wrong dimensions.

        """
        if len(val) != self.objective_vectors.shape[1]:
            msg = ("The ideal point's length '{}' must match the number of "
                   "objectives '{}' (columns) specified in the given "
                   "pareto front.").format(len(val),
                                           self.objective_vectors.shape[1])
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        self.__ideal = val
Пример #10
0
    def n_points(self, val: int):
        """Set the desired number of solutions to be generated each
        iteration. Must be between 1 and 4 (inclusive)

        Args:
            val(int): The number of points to be generated.

        Raises:
            InteractiveMethodError: The number of points to be generated is not
            between 1 and 4.

        """
        if val < 1 or val > 4:
            msg = (
                "The given number '{}' of solutions to be generated is not"
                " between 1 and 4 (inclusive)"
            ).format(val)
            logger.error(msg)
            raise InteractiveMethodError(msg)
        self.__n_points = val
Пример #11
0
    def current_point(self, val: np.ndarray):
        """Set the current point for the algorithm. The dimensions of the
        current point must match the dimensions of the row vectors in
        objective_vectors.

        Args:
            val(np.ndarray): The current point.

        Raises:
            InteractiveMethodError: The current point's dimension does not
            match that of the given pareto optimal objective vectors.

        """
        if len(val) != self.objective_vectors.shape[1]:
            msg = (
                "Current point dimensions '{}' don't match the objective"
                " vector dimensions '{}'"
            ).format(len(val), self.objective_vectors.shape[1])
            logger.error(msg)
            raise InteractiveMethodError(msg)

        self.__current_point = val
Пример #12
0
    def _sort_classsifications(self):
        """Sort the objective indices into their corresponding sets and save
        the aspiration and upper bounds set in the classifications.

        Raises:
            InteractiveMethodError: A classification is found to be ill-formed.

        """
        # empty the sets and bounds
        self.__ind_set_lt = []
        self.__ind_set_lte = []
        self.__ind_set_eq = []
        self.__ind_set_gte = []
        self.__ind_set_free = []
        aspiration_levels = []
        upper_bounds = []
        for (ind, cls) in enumerate(self.classifications):
            if cls[0] == "<":
                self.__ind_set_lt.append(ind)
            elif cls[0] == "<=":
                self.__ind_set_lte.append(ind)
                aspiration_levels.append(cls[1])
            elif cls[0] == "=":
                self.__ind_set_eq.append(ind)
            elif cls[0] == ">=":
                self.__ind_set_gte.append(ind)
                upper_bounds.append(cls[1])
            elif cls[0] == "0":
                self.__ind_set_free.append(ind)
            else:
                msg = (
                    "Check that the classification '{}' is correct."
                ).format(cls)
                logger.error(msg)
                raise InteractiveMethodError(msg)
        self.__aspiration_levels = np.array(aspiration_levels)
        self.__upper_bounds = np.array(upper_bounds)
Пример #13
0
    def classifications(self, val: List[Tuple[str, Optional[float]]]):
        """Parses classifications and checks if they are sensical. See
        `Miettinen 2016`_

        Args:
            val (List[Tuple, Optional[float]]): The classificaitons. The first
            element is the class and the second element is auxilliary
            information needed by some classifications.

        Raises:
            InteractiveMethodError: The classifications given are ill-formed.

        """
        if len(val) != self.objective_vectors.shape[1]:
            msg = (
                "Each of the objective functions must be classified. Check "
                "that '{}' has a correct amount (in this case {}) of "
                "elements in it."
            ).format(val, self.objective_vectors.shape[1])
            logger.error(msg)
            raise InteractiveMethodError(msg)

        if not all(
            [cls[0] in self.__available_classifications for cls in val]
        ):
            msg = (
                "Check the given classifications '{}'. The first element of "
                "each tuple should be found in '{}'"
            ).format(val, self.__available_classifications)
            logger.error(msg)
            raise InteractiveMethodError(msg)

        clss = [cls[0] for cls in val]
        if not (
            ("<" in clss or "<=" in clss) and (">=" in clss or "0" in clss)
        ):
            msg = (
                "Check the calssifications '{}'. At least one of the "
                "objectives should able to be improved and one of the "
                "objectives should be able to deteriorate."
            ).format(val)
            logger.error(msg)
            raise InteractiveMethodError(msg)

        for (ind, cls) in enumerate(val):
            if cls[0] == "<=":
                if not cls[1] < self.current_point[ind]:
                    msg = (
                        "For the '{}th' objective, the aspiration level '{}' "
                        "must be smaller than the current value of the "
                        "objective '{}'."
                    ).format(ind, cls[1], self.current_point[ind])
                    logger.error(msg)
                    raise InteractiveMethodError(msg)
            elif cls[0] == ">=":
                if not cls[1] > self.current_point[ind]:
                    msg = (
                        "For the '{}th' objective, the upper bound '{}' "
                        "must be greater than the current value of the "
                        "objective '{}'."
                    ).format(ind, cls[1], self.current_point[ind])
                    logger.error(msg)
                    raise InteractiveMethodError(msg)

        self.__classifications = val
Пример #14
0
    def interact(
        self,
        index_set: np.ndarray = None,
        percentages: np.ndarray = None,
        use_previous_preference: bool = False,
        new_remaining_iterations: Optional[int] = None,
        step_back: bool = False,
        short_step: bool = False,
    ) -> Union[int, Tuple[np.ndarray, np.ndarray]]:
        """Handle user preference and set appropiate flags for the next iteration.

        Args:
            index_set (np.ndarray): An array with integers describing the
            relative importance of each objective. The integers vary between 1
            and the maximum number of objectives in the problem.
            percentages (np.ndarray): Percentages describing the absolute
            importance of each objective. The sum of these must equal 100.
            use_previous_preference (bool): Use the preference
            infromation. Cannot be true during the first iteration.
            defined in the last iteration. Defaults to false.
            new_remaining_iterations (int): Set a new number of remaining
            iterations to be carried. Must be positive and not exceed the
            current number of iterations left.
            step_back (bool): Step from the previous point in the next
            iteration. Cannot step back from first iteration.
            short_step (bool): When step_back, take a shorter step in the same
            direction as in the previous iteration from the previous
            iteration's iteration point. Can only short step when stepping
            back.

        Returns:
            Union[int, Tuple[np.ndarray, np.ndarray]]: The number of remaining
            iteration. If this function is envoked after the last iteration,
            returns a tuple with the optimal solution and objective values.

        Raises:
            InteractiveMethodError: Some of the arguments are not set
            correctly. See the documentation for the arguments.

        """
        if index_set is not None:
            self.preference_index_set = index_set
            self.__use_previous_preference = False

        elif percentages is not None:
            self.preference_percentages = percentages
            self.__use_previous_preference = False

        elif self.mu is not None and use_previous_preference:
            self.__use_previous_preference = use_previous_preference

        elif step_back and short_step:
            # no preference needed for short step
            pass

        else:
            msg = "Cannot figure out preference infromation."
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        if new_remaining_iterations is not None:
            self.ith = new_remaining_iterations

        if not step_back:
            self.__step_back = False
            # Advance the current iteration, if not the first iteration
            if not self.__first_iteration:
                self.ith -= 1
                self.h += 1
            else:
                self.__first_iteration = False

            if self.ith == 0:
                # Last iteration, terminate the solution
                return (self.xs[self.h], self.fs[self.h])

        else:
            if self.__first_iteration:
                msg = "Cannot take a backwards step on the first iteration."
                logger.debug(msg)
                raise InteractiveMethodError(msg)
            self.__step_back = True

        if short_step:
            if not step_back:
                msg = ("Can take a short step only when stepping from the "
                       "previous point.")
                logger.debug(msg)
                raise InteractiveMethodError(msg)
        self.__short_step = short_step

        return self.ith
Пример #15
0
    def iterate(self) -> Tuple[np.ndarray, List[Tuple[float, float]], float]:
        """Iterate once according to the user preference given in the
        interaction phase.

        Returns:
            Tuple[np.ndarray, List[Tuple[float, float]], float]: A tuple
            containing:
                np.ndarray: The current iteration point.
                List[Tuple[float, float]]: A list with tuples with the lower
                and upper bounds for the next iteration
                float: The distance of the current iteration point to the
                pareto optimal set.

        Raises:
            InteractiveMethodError: If the preferential factors can't be
            computed

        Note:
            The current iteration is to be interpreted as self.h + 1, since
            incrementation of the current iteration happens in the interaction
            phase.

            If both the relative importance and percentages are defined,
            percentages are used.

        """
        # Calculate the preferential factors or use existing ones
        if not self.__short_step:
            if self.preference_percentages is not None:
                # use percentages to calcualte the new iteration point
                delta_q = self.preference_percentages / 100
                self.mu = 1 / (delta_q * (self.problem.nadir -
                                          (self.problem.ideal - self.epsilon)))

            elif self.preference_index_set is not None:
                # Use the relative importance to calcualte the new points
                print(self.preference_index_set)
                for (i, r) in enumerate(self.preference_index_set):
                    self.mu[i] = 1 / (r *
                                      (self.problem.nadir[i] -
                                       (self.problem.ideal[i] - self.epsilon)))

            elif self.__use_previous_preference:
                # previous
                pass

            else:
                msg = "Could not compute the preferential factors."
                logger.debug(msg)
                raise InteractiveMethodError(msg)

        if not self.__short_step and not self.__use_previous_preference:
            # Take a normal step and calculate a new reference point
            # set the current iteration point as the reference point
            self.q = self.zs[self.h]

            # set the preferential factors in the underlaying asf
            self.asf.preferential_factors = self.mu

            # solve the ASF
            (solution, (objective, _)) = self.scalar_solver.solve(self.q)

            # Store the solution and corresponding objective vector
            self.xs[self.h + 1] = solution
            self.fs[self.h + 1] = objective[0]

            # calculate a new iteration point
            self.zs[self.h + 1] = self._calculate_iteration_point()

        elif not self.__step_back and self.__use_previous_preference:
            # Use the solution and objective of the last step
            self.xs[self.h + 1] = self.xs[self.h]
            self.fs[self.h + 1] = self.fs[self.h]
            self.zs[self.h + 1] = self._calculate_iteration_point()

        else:
            # Take a short step
            # Update the current iteration point
            self.zs[self.h + 1] = (0.5 * self.zs[self.h + 1] +
                                   0.5 * self.zs[self.h])

        # calculate the lower bounds for the next iteration
        if self.ith > 1:
            # last iteration, no need to calculate these
            self.lower_bounds[self.h + 1] = np.zeros(
                self.problem.n_of_objectives)

            self.__epsilon_solver.epsilons = self.zs[self.h + 1]

            for r in range(self.problem.n_of_objectives):
                (_, (objective, _)) = self.__epsilon_solver.solve(r)
                self.lower_bounds[self.h + 1][r] = objective[0][r]

                # set the upper bounds
                self.upper_bounds[self.h + 1] = self.zs[self.h]

        else:
            self.lower_bounds[self.h +
                              1] = [None] * self.problem.n_of_objectives
            self.upper_bounds[self.h +
                              1] = [None] * self.problem.n_of_objectives

        # Calculate the distance to the pareto optimal set
        self.ds[self.h + 1] = self._calculate_distance()

        return (
            self.zs[self.h + 1],
            list(
                zip(
                    self.lower_bounds[self.h + 1],
                    self.upper_bounds[self.h + 1],
                )),
            self.ds[self.h + 1],
        )
Пример #16
0
    def interact(  # type: ignore
            self, preferred_point: np.ndarray,
            lower_bounds: np.ndarray) -> Union[int, Tuple[np.ndarray,
                                                          np.ndarray]]:
        """Specify the next preferred point from which to iterate in the next
        iteration. The lower bounds of the reachable values from the preferred
        point are also expected. This point does not necessarely need to be a
        point returned by the iterate method in this class.

        Args:
            preferred_point (np.ndarray): An objective value vector
            representing the preferred point.
            lower_bounds (np.ndarray): The lower bounds of the reachable values
            from the preferred point.

        Returns:
            Union[int, Tuple[np.ndarray, np.ndarray]]: The number of iterations
                left, if not invoked on the last iteration. Otherwise a tuple
                containing: np.ndarray: The final pareto optimal solution.
                np.ndarray: The corresponding objevtive vector to the pareto
                optimal solution.

        Raises:
            InteractiveMethodError: The dimensions of either the preferred
            point or the lower bounds of the reachable values are incorrect.

        """
        if len(preferred_point) != self.objective_vectors.shape[1]:
            # check that the dimensions of the given points are correct
            msg = ("The dimensions of the prefered point '{}' do not match "
                   "the shape of the objective vectors '{}'.").format(
                       len(preferred_point), self.objective_vectors.shape[1])
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        if len(lower_bounds) != self.objective_vectors.shape[1]:
            msg = ("The dimensions of the lower bounds for the prefered "
                   "point '{}' do not match "
                   "the shape of the objective vectors '{}'.").format(
                       len(lower_bounds), self.objective_vectors.shape[1])
            logger.debug(msg)
            raise InteractiveMethodError(msg)

        self.zpref = preferred_point

        if self.ith <= 1:
            # stop the algorithm and return the final solution and the
            # corresponding objective vector
            idx = np.linalg.norm(self.obj_sub[self.h] - self.zpref,
                                 axis=1).argmin()
            self.ith = 0
            return self.par_sub[self.h][idx], self.obj_sub[self.h][idx]

        # Calculate the new reachable pareto solutions and objective vectors
        # from zpref
        cond1 = np.all(np.less_equal(lower_bounds, self.obj_sub[self.h]),
                       axis=1)
        cond2 = np.all(np.less_equal(self.obj_sub[self.h], self.zpref), axis=1)

        indices = (cond1 & cond2).nonzero()

        self.obj_sub[self.h + 1] = self.obj_sub[self.h][indices]
        self.par_sub[self.h + 1] = self.par_sub[self.h][indices]

        self.ith -= 1
        self.h += 1

        return self.ith