Exemplo n.º 1
0
    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([])
Exemplo n.º 2
0
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