import numpy as np

from pypop7.optimizers.core.optimizer import Optimizer


class EDA(Optimizer):
    """Estimation of distribution algorithms(EDA)

probabilistic graphical models

    Based on Genetic Algorithms (GAs), the new class of algorithms generalizes GAs by replacing the crossover and mutation operators by learning and sampling the probability distribution of the best individuals of the population at each iterations of the algorithm

working in such a way, the relationships between the variables involved in the problem domain are explicitly and effectively captured and exploited

AKA probabilistic model-building genetic algorithms (PMBGAs), iterated density estimation evolutionary algorithms (IDEAs)

EDAs were introduced in the field of evolutionary
computation for the first time by Miihlenbein and PaaB (1996)

The main problem with EDAs is how the probability distribution Pl(X) is estimated.

    Reference
    --------------
    P. A. N. Bosman, D. Thierens
    Expanding from Discrete to Continuous Estimation of Distribution Algorithms: The IDEA
    PPSN 2000: 767-776

    Selection and reinforcement learning for combinatorial optimization.
    """
    def __init__(self, problem, options):
        Optimizer.__init__(self, problem, options)
        self._n_generations = 0
        if self.n_individuals is None:  # number of offspring, offspring population size (λ: lambda)
            self.n_individuals = 4 + int(3 * np.log(self.ndim_problem))  # for small population setting
        if self.n_parents is None:  # number of parents, parental population size (μ: mu)
            self.n_parents = int(self.n_individuals / 2)

    def initialize(self):
        raise NotImplementedError

    def iterate(self):
        raise NotImplementedError

    def _print_verbose_info(self, y):
        if self.verbose and (not self._n_generations % self.verbose_frequency):
            best_so_far_y = -self.best_so_far_y if self._is_maximization else self.best_so_far_y
            info = '  * Generation {:d}: best_so_far_y {:7.5e}, min(y) {:7.5e} & Evaluations {:d}'
            print(info.format(self._n_generations, best_so_far_y, np.min(y), self.n_function_evaluations))

    def _initialize_x(self, is_restart=False):
        if is_restart or (self.x is None):
            x = self.rng_initialization.uniform(self.initial_lower_boundary,
                                                self.initial_upper_boundary)
        else:
            x = np.copy(self.x)
        return x

    def _collect_results(self, fitness, mean=None):
        results = Optimizer._collect_results(self, fitness)
        results['mean'] = mean
        results['sigma'] = self.sigma
        results['_n_generations'] = self._n_generations
        results['n_restart'] = self.n_restart
        return results
