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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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], )
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