def __init__(self, *args, **kwargs): """ Input ---------- spot_setup: class model: function Should be callable with a parameter combination of the parameter-function and return an list of simulation results (as long as evaluation list) parameter: function When called, it should return a random parameter combination. Which can be e.g. uniform or Gaussian objectivefunction: function Should return the objectivefunction for a given list of a model simulation and observation. evaluation: function Should return the true values as return by the model. dbname: str * Name of the database where parameter, objectivefunction value and simulation results will be saved. dbformat: str * ram: fast suited for short sampling time. no file will be created and results are saved in an array. * csv: A csv file will be created, which you can import afterwards. parallel: str * seq: Sequentiel sampling (default): Normal iterations on one core of your cpu. * mpi: Message Passing Interface: Parallel computing on cluster pcs (recommended for unix os). save_sim: boolean * True: Simulation results will be saved * False: Simulation results will not be saved :param r: neighborhood size perturbation parameter (r) that defines the random perturbation size standard deviation as a fraction of the decision variable range. Default is 0.2. :type r: float """ try: self.r = kwargs.pop("r") except KeyError: self.r = 0.2 # default value super(padds, self).__init__(*args, **kwargs) self.np_random = np.random self.best_value = BestValue(self.parameter, None) self.dds_generator = DDSGenerator(self.np_random) # self.generator_repetitions will be set in `sample` and is needed to generate a # generator which sends back actual parameter s_test self.generator_repetitions = -1 self.pareto_front = np.array([]) self.dominance_flag = -2 self.obj_func_current = None self.parameter_current = None # because we have a pareto front we need another save type self.like_struct_typ = type([])
class padds(_algorithm): """ Implements the Pareto Archived Dynamically Dimensioned Search (short PADDS algorithm) by Tolson, B. A. and Asadzadeh M. (2013) https://www.researchgate.net/publication/259982925_Pareto_archived_dynamically_dimensioned_search_with_hypervolume-based_selection_for_multi-objective_optimization PADDS using the DDS algorithm with a pareto front included. Two metrics are implemented, which is the simple "one" metric and the "crowd distance" metric. """ def __init__(self, *args, **kwargs): """ Input ---------- spot_setup: class model: function Should be callable with a parameter combination of the parameter-function and return an list of simulation results (as long as evaluation list) parameter: function When called, it should return a random parameter combination. Which can be e.g. uniform or Gaussian objectivefunction: function Should return the objectivefunction for a given list of a model simulation and observation. evaluation: function Should return the true values as return by the model. dbname: str * Name of the database where parameter, objectivefunction value and simulation results will be saved. dbformat: str * ram: fast suited for short sampling time. no file will be created and results are saved in an array. * csv: A csv file will be created, which you can import afterwards. parallel: str * seq: Sequentiel sampling (default): Normal iterations on one core of your cpu. * mpi: Message Passing Interface: Parallel computing on cluster pcs (recommended for unix os). save_sim: boolean * True: Simulation results will be saved * False: Simulation results will not be saved :param r: neighborhood size perturbation parameter (r) that defines the random perturbation size standard deviation as a fraction of the decision variable range. Default is 0.2. :type r: float """ try: self.r = kwargs.pop("r") except KeyError: self.r = 0.2 # default value self._return_all_likes = True #allows multi-objective calibration kwargs['optimization_direction'] = 'minimize' kwargs[ 'algorithm_name'] = 'Pareto Archived Dynamically Dimensioned Search (PADDS) algorithm' super(padds, self).__init__(*args, **kwargs) self.np_random = np.random self.best_value = BestValue(self.parameter, None) self.dds_generator = DDSGenerator(self.np_random) # self.generator_repetitions will be set in `sample` and is needed to generate a # generator which sends back actual parameter s_test self.generator_repetitions = -1 self.pareto_front = np.array([]) self.dominance_flag = -2 self.obj_func_current = None self.parameter_current = None # because we have a pareto front we need another save type self.like_struct_typ = type([]) def _set_np_random(self, f_rand): self.np_random = f_rand if hasattr(self, "hvc"): self.hvc._set_np_random(f_rand) self.dds_generator.np_random = f_rand def roulette_wheel(self, metric): cumul_metric = np.cumsum(metric) probability = self.np_random.rand() * cumul_metric[-1] levels = (cumul_metric >= probability) length = cumul_metric.shape[0] return np.array(range(length))[levels][0] def get_next_x_curr(self): """ Fake a generator to run self.repeat to use multiprocessing """ # We need to shift position and length of the sampling process for rep in range(self.generator_repetitions): if self.dominance_flag == -1: # if the last generated solution was dominated index = self.roulette_wheel(self.metric) self.best_value.parameters, self.best_value.best_obj_val = self.pareto_front[ index][1], self.pareto_front[index][0] else: # otherwise use the last generated solution self.best_value.parameters, self.best_value.best_obj_val = ( self.parameter_current, self.obj_func_current) # # This line is needed to get an array of data converted into a parameter object self.best_value.fix_format() yield rep, self.calculate_next_s_test(self.best_value.parameters, rep, self.generator_repetitions, self.r) def calculate_initial_parameterset(self, repetitions, initial_objs, initial_params): self.obj_func_current = np.array([0.0]) self.parameter_current = np.array([0.0] * self.number_of_parameters) self.parameter_range = self.best_value.parameters.maxbound - self.best_value.parameters.minbound self.pareto_front = np.array( [[np.array([]), np.array([0] * self.number_of_parameters)]]) #self.pareto_front = np.array([np.append([np.inf] * self.like_struct_len, [0] * self.number_of_parameters)]) if (len(initial_objs) != len(initial_params)): raise ValueError( "User specified 'initial_objs' and 'initial_params' have no equal length" ) if len(initial_objs) == 0: initial_iterations = np.int(np.max([5, round(0.005 * repetitions)])) self.calc_initial_pareto_front(initial_iterations) elif initial_params.shape[1] != self.number_of_parameters: raise ValueError( "User specified 'initial_params' has not the same length as available parameters" ) else: if not (np.all( initial_params <= self.best_value.parameters.maxbound) and np.all( initial_params >= self.best_value.parameters.minbound) ): raise ValueError( "User specified 'initial_params' but the values are not within the parameter range" ) initial_iterations = initial_params.shape[0] for i in range(initial_params.shape[0]): self.parameter_current = initial_params[i] if len(initial_objs[i]) > 0: self.obj_func_current = np.array(initial_objs[i]) else: self.obj_func_current = np.array( self.getfitness(simulation=[], params=self.parameter_current)) if i == 0: # Initial value self.pareto_front = np.array( [[self.obj_func_current, self.parameter_current]]) dominance_flag = 1 else: self.pareto_front, dominance_flag = nd_check( self.pareto_front, self.obj_func_current, self.parameter_current.copy()) self.dominance_flag = dominance_flag return initial_iterations, copy.deepcopy(self.parameter_current) def sample(self, repetitions, trials=1, initial_objs=np.array([]), initial_params=np.array([]), metric="ones"): # every iteration a map of all relevant values is stored, only for debug purpose. # Spotpy will not need this values. debug_results = [] print('Starting the PADDS algotrithm with ' + str(repetitions) + ' repetitions...') print( 'WARNING: THE PADDS algorithm as implemented in SPOTPY is in an beta stage and not ready for production use!' ) self.set_repetiton(repetitions) self.number_of_parameters = len( self.best_value.parameters ) # number_of_parameters is the amount of parameters if metric == "hvc": self.hvc = HVC(np_random=self.np_random) self.min_bound, self.max_bound = self.parameter( )['minbound'], self.parameter()['maxbound'] # Users can define trial runs in within "repetition" times the algorithm will be executed for trial in range(trials): self.best_value.best_obj_val = 1e-308 repitionno_best, self.best_value.parameters = self.calculate_initial_parameterset( repetitions, initial_objs, initial_params) repetions_left = repetitions - repitionno_best # Main Loop of PA-DDS self.metric = self.calc_metric(metric) # important to set this field `generator_repetitions` so that # method `get_next_s_test` can generate exact parameters self.generator_repetitions = repetions_left for rep, x_curr, simulations in self.repeat( self.get_next_x_curr()): obj_func_current = self.postprocessing(rep, x_curr, simulations) self.obj_func_current = np.array(obj_func_current) num_imp = np.sum( self.obj_func_current <= self.best_value.best_obj_val) num_deg = np.sum( self.obj_func_current > self.best_value.best_obj_val) if num_imp == 0 and num_deg > 0: self.dominance_flag = -1 # New solution is dominated by its parents else: # Do dominance check only if new solution is not dominated by its parent self.pareto_front, self.dominance_flag = nd_check( self.pareto_front, self.obj_func_current, x_curr.copy()) if self.dominance_flag != -1: # means, that new parameter set is a new non-dominated solution self.metric = self.calc_metric(metric) self.parameter_current = x_curr # update the new status structure self.status.params_max, self.status.params_min = self.parameter_current, self.parameter_current print('Best solution found has obj function value of ' + str(self.best_value.best_obj_val) + ' at ' + str(repitionno_best) + '\n\n') debug_results.append({ "sbest": self.best_value.parameters, "objfunc_val": self.best_value.best_obj_val }) self.final_call() return debug_results def calc_metric(self, metric): """ calculate / returns metric field :return: set of metric of choice """ if metric == "ones": return np.array([1] * self.pareto_front.shape[0]) elif metric == "crowd_distance": return crowd_dist(np.array([w for w in self.pareto_front[:, 0]])) elif metric == "chc": return chc(np.array([w for w in self.pareto_front[:, 0]])) elif metric == "hvc": return self.hvc(np.array([w for w in self.pareto_front[:, 0]])) else: raise AttributeError("metric argument is invalid") def calc_initial_pareto_front(self, its): """ calculate the initial pareto front :param its: amount of initial parameters """ dominance_flag = -1 for i in range(its): for j in range(self.number_of_parameters): if self.best_value.parameters.as_int[j]: self.parameter_current[j] = self.np_random.randint( self.best_value.parameters.minbound[j], self.best_value.parameters.maxbound[j]) else: self.parameter_current[ j] = self.best_value.parameters.minbound[ j] + self.parameter_range[j] * self.np_random.rand( ) # uniform random id, params, model_simulations = self.simulate( (range(len(self.parameter_current)), self.parameter_current)) self.obj_func_current = self.getfitness( simulation=model_simulations, params=self.parameter_current) # First value will be used to initialize the values if i == 0: self.pareto_front = np.vstack([ self.pareto_front[0], np.array([ self.obj_func_current.copy(), self.parameter_current.copy() + 0 ]) ]) else: (self.pareto_front, dominance_flag) = nd_check(self.pareto_front, self.obj_func_current, self.parameter_current.copy()) self.dominance_flag = dominance_flag def calculate_next_s_test(self, previous_x_curr, rep, rep_limit, r): """ Needs to run inside `sample` method. Calculate the next set of parameters based on a given set. This is greedy algorithm belonging to the DDS algorithm. `probability_neighborhood` is a threshold at which level a parameter is added to neighbourhood calculation. Using a normal distribution The decision variable `dvn_count` counts how many parameter configuration has been exchanged with neighbourhood values. If no parameters has been exchanged just one will select and exchanged with it's neighbourhood value. :param previous_x_curr: A set of parameters :param rep: Position in DDS loop :param r: neighbourhood size perturbation parameter :return: next parameter set """ amount_params = len(previous_x_curr) new_x_curr = previous_x_curr.copy( ) # define new_x_curr initially as current (previous_x_curr for greedy) randompar = self.np_random.rand(amount_params) probability_neighborhood = 1.0 - np.log(rep + 1) / np.log(rep_limit) dvn_count = 0 # counter for how many decision variables vary in neighbour for j in range(amount_params): if randompar[ j] < probability_neighborhood: # then j th DV selected to vary in neighbour dvn_count = dvn_count + 1 new_value = self.dds_generator.neigh_value_mixed( previous_x_curr, r, j, self.min_bound[j], self.max_bound[j]) new_x_curr[ j] = new_value # change relevant dec var value in x_curr if dvn_count == 0: # no DVs selected at random, so select ONE dec_var = np.int(np.ceil(amount_params * self.np_random.rand())) new_value = self.dds_generator.neigh_value_mixed( previous_x_curr, r, dec_var - 1, self.min_bound[dec_var - 1], self.max_bound[dec_var - 1]) new_x_curr[ dec_var - 1] = new_value # change relevant decision variable value in s_test return new_x_curr