getGlobalMaxEvaluation() (macop.algorithms.base.Algorithm method)

initalizer (macop.algorithms.mono.HillClimberBestImprovment attribute)

evaluator (macop.algorithms.mono.HillClimberBestImprovment attribute)

operators (macop.algorithms.mono.HillClimberBestImprovment attribute)

policy (macop.algorithms.mono.HillClimberBestImprovment attribute)

validator (macop.algorithms.mono.HillClimberBestImprovment attribute)

maximise (macop.algorithms.mono.HillClimberBestImprovment attribute)

currentSolution (macop.algorithms.mono.HillClimberBestImprovment attribute)

bestSolution (macop.algorithms.mono.HillClimberBestImprovment attribute)

callbacks (macop.algorithms.mono.HillClimberBestImprovment attribute)

initalizer (macop.algorithms.mono.HillClimberFirstImprovment attribute)

evaluator (macop.algorithms.mono.HillClimberFirstImprovment attribute)

operators (macop.algorithms.mono.HillClimberFirstImprovment attribute)

policy (macop.algorithms.mono.HillClimberFirstImprovment attribute)

validator (macop.algorithms.mono.HillClimberFirstImprovment attribute)

maximise (macop.algorithms.mono.HillClimberFirstImprovment attribute)

currentSolution (macop.algorithms.mono.HillClimberFirstImprovment attribute)

bestSolution (macop.algorithms.mono.HillClimberFirstImprovment attribute)

callbacks (macop.algorithms.mono.HillClimberFirstImprovment attribute)

initalizer (macop.algorithms.mono.IteratedLocalSearch attribute)

currentSolution (macop.algorithms.mono.IteratedLocalSearch attribute)

bestSolution (macop.algorithms.mono.IteratedLocalSearch attribute)

RandomSplitCrossover (class in macop.operators.discrete.crossovers)

kind (macop.operators.discrete.crossovers.RandomSplitCrossover attribute)

apply() (macop.operators.discrete.crossovers.RandomSplitCrossover method)

kind (macop.operators.discrete.crossovers.SimpleCrossover attribute)

apply() (macop.operators.discrete.crossovers.SimpleCrossover method)

SimpleBinaryMutation (class in macop.operators.discrete.mutators)

kind (macop.operators.discrete.mutators.SimpleBinaryMutation attribute)

apply() (macop.operators.discrete.mutators.SimpleBinaryMutation method)

kind (macop.operators.discrete.mutators.SimpleMutation attribute)

apply() (macop.operators.discrete.mutators.SimpleMutation method)

random() (macop.solutions.discrete.BinarySolution static method)

data (macop.solutions.discrete.CombinatoryIntegerSolution attribute)

size (macop.solutions.discrete.CombinatoryIntegerSolution attribute)

score (macop.solutions.discrete.CombinatoryIntegerSolution attribute)

random() (macop.solutions.discrete.CombinatoryIntegerSolution static method)

random() (macop.solutions.discrete.IntegerSolution static method)

"""Basic Algorithm class
"""

# main imports
import logging
import sys, os
from ..utils.progress import macop_text, macop_line, macop_progress

# Generic algorithm class
class Algorithm():
    """Abstract Algorithm class used as basic algorithm implementation with some specific initialization

    This class enables to manage some common usages of operation research algorithms:
    - initialization function of solution
    - validator function to check if solution is valid or not (based on some criteria)
    - evaluation function to give fitness score to a solution
    - operators used in order to update solution during search process
    - policy process applied when choosing next operator to apply
    - callbacks function in order to do some relative stuff every number of evaluation or reload algorithm state
    - parent algorithm associated to this new algorithm instance (hierarchy management)

    Attributes:
        initializer: {function} -- basic function strategy to initialize solution
        evaluator: {function} -- basic function in order to obtained fitness (mono or multiple objectives)
        operators: {[Operator]} -- list of operator to use when launching algorithm
        policy: {Policy} -- Policy class implementation strategy to select operators
        validator: {function} -- basic function to check if solution is valid or not under some constraints
        maximise: {bool} -- specify kind of optimisation problem 
        verbose: {bool} -- verbose or not information about the algorithm
        currentSolution: {Solution} -- current solution managed for current evaluation comparison
        bestSolution: {Solution} -- best solution found so far during running algorithm
        callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
        parent: {Algorithm} -- parent algorithm reference in case of inner Algorithm instance (optional)
    """
    def __init__(self,
                 initializer,
                 evaluator,
                 operators,
                 policy,
                 validator,
                 maximise=True,
                 parent=None,
                 verbose=True):

        # protected members intialization
        self._initializer = initializer
        self._evaluator = evaluator
        self._operators = operators
        self._policy = policy
        self._validator = validator
        self._callbacks = []
        self._bestSolution = None
        self._currentSolution = None

        # by default
        self._numberOfEvaluations = 0
        self._maxEvaluations = 0

        # other parameters
        self._parent = parent  # parent algorithm if it's sub algorithm

        #self.maxEvaluations = 0 # by default
        self._maximise = maximise

        self._verbose = verbose

        # track reference of algorihtm into operator (keep an eye into best solution)
        for operator in self._operators:
            if self._parent is not None:
                operator.setAlgo(self.getParent())
            else:
                operator.setAlgo(self)

        # also track reference for policy
        if self._parent is not None:
            self._policy.setAlgo(self.getParent())
        else:
            self._policy.setAlgo(self)

    def addCallback(self, _callback):
        """Add new callback to algorithm specifying usefull parameters

        Args:
            _callback: {Callback} -- specific Callback instance
        """
        # specify current main algorithm reference for callback
            _callback.setAlgo(self.getParent())
        else:
            _callback.setAlgo(self)

        # set as new
        self._callbacks.append(_callback)

    def resume(self):
        """Resume algorithm using Callback instances
        """

        # load every callback if many things are necessary to do before running algorithm
        for callback in self._callbacks:
            callback.load()

    def getParent(self):
        """Recursively find the main parent algorithm attached of the current algorithm

        Returns:
            {Algorithm} -- main algorithm set for this algorithm
        """

        current_algorithm = self
        parent_alrogithm = None

        # recursively find the main algorithm parent
        while current_algorithm._parent is not None:
            parent_alrogithm = current_algorithm._parent
            current_algorithm = current_algorithm._parent

        return parent_alrogithm

    def initRun(self):
        """
        Initialize the current solution and best solution using the `initialiser` function
        """

        self._currentSolution = self._initializer()

        # evaluate current solution
        self._currentSolution.evaluate(self._evaluator)
        self.increaseEvaluation()

        # keep in memory best known solution (current solution)
        if self._bestSolution is None:
            self._bestSolution = self._currentSolution

    def increaseEvaluation(self):
        """
        Increase number of evaluation once a solution is evaluated for each dependant algorithm (parents hierarchy)
        """

        while current_algorithm is not None:

            current_algorithm._numberOfEvaluations += 1
            
    def getGlobalEvaluation(self):
        """Get the global number of evaluation (if inner algorithm)

        Returns:
            {int} -- current global number of evaluation
        """
        parent_algorithm = self.getParent()

        if parent_algorithm is not None:
            return parent_algorithm.getGlobalEvaluation()

        return self._numberOfEvaluations

    def getGlobalMaxEvaluation(self):
        """Get the global max number of evaluation (if inner algorithm)

        Returns:
            {int} -- current global max number of evaluation
        """

            return parent_algorithm.getGlobalMaxEvaluation()

        return self._maxEvaluations

    def stop(self):
        """
        Global stopping criteria (check for parents algorithm hierarchy too)
        """

        # based on global stopping creteria or on its own stopping critera
            return parent_algorithm._numberOfEvaluations >= parent_algorithm._maxEvaluations or self._numberOfEvaluations >= self._maxEvaluations

        return self._numberOfEvaluations >= self._maxEvaluations

    def evaluate(self, solution):
        """
        Evaluate a solution using evaluator passed when intialize algorithm

        Args:
            solution: {Solution} -- solution to evaluate

        Returns: 
            {float} -- fitness score of solution which is not already evaluated or changed

        Note: 
            if multi-objective problem this method can be updated using array of `evaluator`
        """
        return solution.evaluate(self._evaluator)

    def update(self, solution):
        """
        Apply update function to solution using specific `policy`
        Check if solution is valid after modification and returns it
        
        Args:
            solution: {Solution} -- solution to update using current policy

        Returns:
            {Solution} -- updated solution obtained by the selected operator
        """

        # two parameters are sent if specific crossover solution are wished
        sol = self._policy.apply(solution)

        # compute fitness of new solution if not already computed
        if sol._score is None:
            sol.evaluate(self._evaluator)

        if (sol.isValid(self._validator)):
            return sol
        else:
            logging.info("-- New solution is not valid %s" % sol)
            return solution

    def isBetter(self, solution):
        """
        Check if solution is better than best found

        - if the new solution is not valid then the fitness comparison is not done
        - fitness comparison is done using problem nature (maximising or minimising)

        Args:
            solution: {Solution} -- solution to compare with best one

        Returns:
            {bool} -- `True` if better
        """
        if not solution.isValid(self._validator):
            return False

        # depending of problem to solve (maximizing or minimizing)
        if self._maximise:
            if solution.fitness() > self._bestSolution.fitness():
                return True
        else:
            if solution.fitness() < self._bestSolution.fitness():

        return False

    def run(self, evaluations):
        """
        Run the specific algorithm following number of evaluations to find optima
        """

        # append number of max evaluation if multiple run called
        self._maxEvaluations += evaluations

        # check if global evaluation is used or not
        if self.getParent() is not None and self.getGlobalEvaluation() != 0:

            # init number evaluations of inner algorithm depending of globalEvaluation
            # allows to restart from `checkpoint` last evaluation into inner algorithm
            rest = self.getGlobalEvaluation() % self._maxEvaluations
            self._numberOfEvaluations = rest

        else:
            self._numberOfEvaluations = 0

        logging.info("Run %s with %s evaluations" %
                     (self.__str__(), evaluations))

    def progress(self):
        """
        Log progress and apply callbacks if necessary
        """
        if len(self._callbacks) > 0:
            for callback in self._callbacks:
                callback.run()

        if self._verbose:
            macop_progress(self, self.getGlobalEvaluation(), self.getGlobalMaxEvaluation())

        logging.info(f"-- {type(self).__name__} evaluation {self._numberOfEvaluations} of {self._maxEvaluations} ({((self._numberOfEvaluations / self._maxEvaluations) * 100):.2f}%) - BEST SCORE {self._bestSolution.fitness()}")

    def end(self):
        """Display end message into `run` method
        """
    
        macop_text(self, f'({type(self).__name__}) Found after {self._numberOfEvaluations} evaluations \n   - {self._bestSolution}')
        macop_line(self)

    def information(self):
        logging.info(f"-- Best {self._bestSolution} - SCORE {self._bestSolution.fitness()}")

    # def __blockPrint(self):
    #     """Private method which disables console prints when running algorithm if specified when instancing algorithm
    #     """
    #     sys.stdout = open(os.devnull, 'w')

    # def __enablePrint(self):
    #     """Private which enables console prints when running algorithm
    #     """
    #     sys.stdout = sys.__stdout__

    # def __del__(self):
    #     # enable prints when object is deleted
    #     if not self._verbose:
    #         self.__enablePrint()

    def __str__(self):
        return f"{type(self).__name__} using {type(self._bestSolution).__name__}"

"""Mono-objective available algorithms
"""

# module imports
from .base import Algorithm

class HillClimberFirstImprovment(Algorithm):
    """Hill Climber First Improvment used as quick exploration optimisation algorithm

    - First, this algorithm do a neighborhood exploration of a new generated solution (by doing operation on the current solution obtained) in order to find a better solution from the neighborhood space.
    - Then replace the current solution by the first one from the neighbordhood space which is better than the current solution.
    - And do these steps until a number of evaluation (stopping criterion) is reached.

        initalizer: {function} -- basic function strategy to initialize solution
        currentSolution: {Solution} -- current solution managed for current evaluation
    """
        """
        Run the local search algorithm

        Args:
            evaluations: {int} -- number of Local search evaluations
            
        Returns:
            {Solution} -- best solution found
        """

        # by default use of mother method to initialize variables
        super().run(evaluations)

        # initialize current solution and best solution
        self.initRun()

        solutionSize = self._currentSolution._size

        # local search algorithm implementation
        while not self.stop():

            for _ in range(solutionSize):

                # update current solution using policy
                newSolution = self.update(self._currentSolution)

                # if better solution than currently, replace it and stop current exploration (first improvment)
                if self.isBetter(newSolution):
                    self._bestSolution = newSolution
                    break

                # increase number of evaluations
                self.increaseEvaluation()

                self.progress()
                logging.info(f"---- Current {newSolution} - SCORE {newSolution.fitness()}")

                # stop algorithm if necessary
                if self.stop():
                    break

            # set new current solution using best solution found in this neighbor search
            self._currentSolution = self._bestSolution

        logging.info(f"End of {type(self).__name__}, best solution found {self._bestSolution}")
        
        return self._bestSolution

class HillClimberBestImprovment(Algorithm):
    """Hill Climber Best Improvment used as exploitation optimisation algorithm

    - First, this algorithm do a neighborhood exploration of a new generated solution (by doing operation on the current solution obtained) in order to find the best solution from the neighborhood space.
    - Then replace the best solution found from the neighbordhood space as current solution to use.

    """
        """

        Args:
            
        Returns:
        """

                # if better solution than currently, replace it

                    break

class IteratedLocalSearch(Algorithm):
    """Iterated Local Search used to avoid local optima and increave EvE (Exploration vs Exploitation) compromise

    """
    def run(self, evaluations, ls_evaluations=100):
        """
        Run the iterated local search algorithm using local search (EvE compromise)

        Args:
            evaluations: {int} -- number of global evaluations for ILS
            ls_evaluations: {int} -- number of Local search evaluations (default: 100)

        Returns:
        """

        # enable resuming for ILS
        self.resume()

        # initialize current solution

        # passing global evaluation param from ILS
        ls = HillClimberFirstImprovment(self._initializer,
                         self._evaluator,
                         self._operators,
                         self._policy,
                         self._validator,
                         self._maximise,
                         verbose=self._verbose,
                         parent=self)

        # add same callbacks
            ls.addCallback(callback)

            # create and search solution from local search
            newSolution = ls.run(ls_evaluations)

            # if better solution than currently, replace it
            if self.isBetter(newSolution):
                self._bestSolution = newSolution

            # number of evaluatins increased from LocalSearch
            # increase number of evaluations and progress are then not necessary there
            #self.increaseEvaluation()
            #self.progress()

            self.information()

        self.end()

"""Multi-objetive classes algorithm
"""

import math
import numpy as np
import sys

from ..evaluators.multi import WeightedSum

class MOEAD(Algorithm):
    """Multi-Ojective Evolutionary Algorithm with Scalar Decomposition

        mu: {int} -- number of sub problems
        T: {[float]} -- number of neightbors for each sub problem
        nObjectives: {int} -- number of objectives (based of number evaluator)
        evaluator: {[function]} -- list of basic function in order to obtained fitness (multiple objectives)
        maximise: {bool} -- specify kind of optimisation problem
        population: [{Solution}] -- population of solution, one for each sub problem
        pfPop: [{Solution}] -- pareto front population
        weights: [[{float}]] -- random weights used for custom mu sub problems
    """
                 mu,
                 T,
                 policy,

        # redefinition of constructor to well use `initRun` method

        self._nObjectives = len(evaluator)

        # track reference of algo into operator (keep an eye into best solution)
            operator.setAlgo(self)

        # by default track reference for policy
        self._policy.setAlgo(self)

        if mu < T:
            raise ValueError('`mu` cannot be less than `T`')

        self._mu = mu
        self._T = T

        # initialize neighbors for each sub problem
        self.setNeighbors()

        weights = []

        if self._nObjectives == 2:

            for i in range(self._mu):
                angle = math.pi / 2 * i / (self._mu - 1)
                weights.append([math.cos(angle), math.sin(angle)])

        elif self._nObjectives >= 3:

            # random weights using uniform
                w_i = np.random.uniform(0, 1, self._nObjectives)
                weights.append(w_i / sum(w_i))
        else:
            raise ValueError('Unvalid number of objectives')

        self._weights = weights

        self._subProblems = []

        for i in range(self._mu):

            # compute weight sum from solution
            sub_evaluator = WeightedSum(data={'evaluators': evaluator, 'weights': weights[i]})

            # intialize each sub problem
            # use copy of list to keep track for each sub problem
            subProblem = MOSubProblem(i, weights[i],
                                      initializer, sub_evaluator,
                                      operators.copy(), policy, validator,
                                      maximise, self)

            self._subProblems.append(subProblem)

        self._population = [None for n in range(self._mu)]
        self._pfPop = []

        # ref point based on number of evaluators
            self._refPoint = [0 for _ in range(self._nObjectives)]
        else:
            self._refPoint = [
                sys.float_info.max for _ in range(self._nObjectives)
            ]

        """
        Method which initialiazes or re-initializes the whole algorithm context specifically for MOEAD
        """
        # initialization is done during run method
        pass

        """

        Args:
            
        Returns:
        """

        # enable callback resume for MOEAD

        # initialize each sub problem if no backup

            if self._subProblems[i]._bestSolution is None:
                self._subProblems[i].run(1)
                self._population[i] = self._subProblems[i]._bestSolution

        # if no backup for pf population
        if len(self._pfPop) == 0:
                self._pfPop.append(self._subProblems[i]._bestSolution)

        # MOEAD algorithm implementation

                # run 1 iteration into sub problem `i`
                spBestSolution = self._subProblems[i]._bestSolution

                self.updateRefPoint(spBestSolution)

                # for each neighbor of current sub problem update solution if better
                improvment = False
                for j in self._neighbors[i]:
                    if spBestSolution.fitness() > self._subProblems[j]._bestSolution.fitness():

                        # create new solution based on current new if better, computes fitness associated to new solution for sub problem
                        newSolution = spBestSolution.clone()

                        # evaluate solution for new sub problem and update as best solution
                        self._subProblems[j].evaluate(newSolution)
                        self._subProblems[j]._bestSolution = newSolution

                        # update population solution for this sub problem
                        self._population[j] = newSolution

                        improvment = True

                # add new solution if improvment is idenfity
                if improvment:
                    self._pfPop.append(spBestSolution)

                # update pareto front
                self._pfPop = self.paretoFront(self._pfPop)

                # add progress here

                    break

        logging.info(f"End of {type(self).__name__}, best solution found {self._population}")

        return self._pfPop

        """
        """

        macop_progress(self, self.getGlobalEvaluation(), self.getGlobalMaxEvaluation())

        logging.info(f"-- {type(self).__name__} evaluation {self._numberOfEvaluations} of {self._maxEvaluations} ({((self._numberOfEvaluations) / self._maxEvaluations * 100.):.2f}%)")

    def setNeighbors(self):

        dmin = dmax = 0

        if self._T % 2 == 1:
            dmin = -int(self._T / 2)
            dmax = int(self._T / 2) + 1
        else:
            dmin = -int(self._T / 2) + 1
            dmax = +self._T / 2

        # init neighbord list
        self._neighbors = [[] for n in range(self._mu)]

        for direction in range(0, -dmin):
            for i in range(self._T):
                self._neighbors[direction].append(i)

        for direction in range(-dmin, self._mu - dmax):
            for i in range(direction + dmin, direction + dmax):

        for direction in range(self._mu - dmax, self._mu):
            for i in range(self._mu - self._T, self._mu):

    def updateRefPoint(self, solution):

            for i in range(len(self._evaluator)):
                if solution._scores[i] > self._refPoint[i]:
                    self._refPoint[i] = solution._scores[i]
        else:
                if solution.scores[i] < self._refPoint[i]:

    def paretoFront(self, population):

        paFront = []
        indexes = []
        nObjectives = len(self._evaluator)
        nSolutions = len(population)

        # find dominated solution
        for i in range(nSolutions):
            for j in range(nSolutions):

                if j in indexes:
                    continue

                # check if solutions are the same
                if all([
                        population[i]._data[k] == population[j]._data[k]
                        for k in range(len(population[i]._data))
                ]):
                    continue

                nDominated = 0

                # check number of dominated objectives of current solution by the others solution
                for k in range(len(self._evaluator)):
                    if self._maximise:
                        if population[i]._scores[k] < population[j]._scores[k]:
                            nDominated += 1
                    else:
                        if population[i]._scores[k] > population[j]._scores[k]:

                if nDominated == nObjectives:
                    indexes.append(i)
                    break

        # store the non dominated solution into pareto front
            if i not in indexes:
                paFront.append(population[i])

        return paFront

        """ 

        macop_text(self, f'({type(self).__name__}) Found after {self._numberOfEvaluations} evaluations')

        for i, solution in enumerate(self._pfPop):
            macop_text(self, f'  - [{i}] {solution._scores} : {solution}')

        logging.info("-- Pareto front :")

            logging.info(f"-- {i}] SCORE {solution._scores} - {solution}")

        return f"{type(self).__name__} using {type(self._population).__name__}"

class MOSubProblem(Algorithm):
    """Specific MO sub problem used into MOEAD

        index: {int} -- sub problem index
        weights: {[float]} -- sub problems objectives weights
    """
                 index,
                 weights,
                 initalizer,
                 policy,

        super().__init__(initalizer, evaluator, operators, policy,
                         validator, maximise, parent)

        self._index = index

        """

        Args:
            evaluations: {int} -- number of evaluations
            
        Returns:
        """

        # initialize solution if necessary
            self.initRun()

        # new operators list keep track of current sub problem
        for op in self._operators:
            op.setAlgo(self)

        for _ in range(evaluations):

            # keep reference of sub problem used

            # update solution using policy
            newSolution = self.update(self._bestSolution)

            # increase number of evaluations
            self.increaseEvaluation()

            self.progress()

            # stop algorithm if necessary
            if self.stop():
                break

            logging.info(f"---- Current {newSolution} - SCORE {newSolution.fitness()}")

            logging.info(f"End of {type(self).__name__}, best solution found {self._bestSolution}")

"""Abstract Checkpoint classes for callback process
"""
import os
from abc import abstractmethod

class Callback():
    """
    Callback abstract class in order to compute some instruction every evaluation

        algo: {Algorithm} -- main algorithm instance reference
        every: {int} -- checkpoint frequency used (based on number of evaluations)
        filepath: {str} -- file path where checkpoints will be saved
    """
    def __init__(self, every, filepath):

        self._algo = None
        self._every = every
        self._filepath = filepath

        # build path if not already exists
        head, _ = os.path.split(self._filepath)

        if not os.path.exists(head):
            os.makedirs(head)

    def setAlgo(self, algo):
        """Specify the main algorithm instance reference

        Args:
            algo: {Algorithm} -- main algorithm instance reference
        """
        self._algo = algo

    @abstractmethod
    def run(self):
        """
        Check if necessary to do backup based on `every` variable
        """
        pass

    def load(self):
        """
        Load last backup line of solution and set algorithm state at this backup
        """
        pass

"""Classical Checkpoints classes implementations
"""

import os

from .base import Callback
from ..utils.progress import macop_text, macop_line

class BasicCheckpoint(Callback):
    """
    BasicCheckpoint is used for loading previous computations and start again after loading checkpoint

    """
        """
        """
        # get current best solution
        solution = self._algo._bestSolution

        currentEvaluation = self._algo.getGlobalEvaluation()

        # backup if necessary
        if currentEvaluation % self._every == 0:

            logging.info("Checkpoint is done into " + self._filepath)

            solutionData = ""
            solutionSize = len(solution._data)

            for index, val in enumerate(solution._data):
                solutionData += str(val)

                if index < solutionSize - 1:
                    solutionData += ' '

            line = str(currentEvaluation) + ';' + solutionData + ';' + str(
                solution.fitness()) + ';\n'

            # check if file exists
            if not os.path.exists(self._filepath):
                with open(self._filepath, 'w') as f:
                    f.write(line)
            else:
                with open(self._filepath, 'a') as f:

        """
        Load last backup line of solution and set algorithm state (best solution and evaluations) at this backup
        """
        if os.path.exists(self._filepath):

            logging.info('Load best solution from last checkpoint')
            with open(self._filepath) as f:

                # get last line and read data
                lastline = f.readlines()[-1]
                data = lastline.split(';')

                # get evaluation  information
                globalEvaluation = int(data[0])

                if self._algo.getParent() is not None:
                    self._algo.getParent()._numberOfEvaluations = globalEvaluation
                else:
                    self._algo._numberOfEvaluations = globalEvaluation

                # get best solution data information
                solutionData = list(map(int, data[1].split(' ')))

                if self._algo._bestSolution is None:
                    self._algo._bestSolution = self._algo._initializer()

                self._algo._bestSolution._data = np.array(solutionData)
                self._algo._bestSolution._score = float(data[2])
      
            macop_line(self._algo)
            macop_text(self._algo, f'Checkpoint found from `{self._filepath}` file.')
            macop_text(self._algo, f'Restart algorithm from evaluation {self._algo._numberOfEvaluations}.')
        else:
            macop_text(self._algo, 'No backup found... Start running algorithm from evaluation 0.')
            logging.info("Can't load backup... Backup filepath not valid in Checkpoint")

        macop_line(self._algo)

"""Multi-objective Checkpoints classes implementations
"""

import os

class MultiCheckpoint(Callback):
    """
    MultiCheckpoint is used for loading previous computations and start again after loading checkpoint

    """
        """
        """
        # get current population
        population = self._algo._population

            with open(self._filepath, 'w') as f:

                for solution in population:
                    solutionData = ""
                    solutionSize = len(solution._data)

                    for index, val in enumerate(solution._data):
                        solutionData += str(val)

                        if index < solutionSize - 1:
                            solutionData += ' '

                    line = str(currentEvaluation) + ';'

                    for i in range(len(self._algo._evaluator)):
                        line += str(solution._scores[i]) + ';'

                    line += solutionData + ';\n'

        """
        Load backup lines as population and set algorithm state (population and pareto front) at this backup
        """

                # read data for each line
                for i, line in enumerate(f.readlines()):

                    data = line.replace(';\n', '').split(';')

                    # only the first time
                    if i == 0:
                        # get evaluation  information
                        globalEvaluation = int(data[0])

                        if self._algo.getParent() is not None:
                            self._algo.getParen()._numberOfEvaluations = globalEvaluation
                        else:
                            self._algo._numberOfEvaluations = globalEvaluation

                    nObjectives = len(self._algo._evaluator)
                    scores = [float(s) for s in data[1:nObjectives + 1]]

                    # get best solution data information
                    solutionData = list(map(int, data[-1].split(' ')))

                    # initialize and fill with data
                    self._algo._population[i] = self._algo._initializer()
                    self._algo._population[i]._data = np.array(solutionData)
                    self._algo._population[i]._scores = scores

                    self._algo._pfPop.append(self._algo._population[i])

            macop_text(self._algo, f'Load of available population from `{self._filepath}`')
        else:

class ParetoCheckpoint(Callback):
    """
    Pareto checkpoint is used for loading previous computations and start again after loading checkpoint

    """
        """
        """
        pfPop = self._algo._pfPop

                for solution in pfPop:

                    line = ''

        """
        """

                    scores = [float(s) for s in data[0:nObjectives]]

                    self._algo._pfPop[i]._data = solutionData
                    self._algo._pfPop[i]._scores = scores

            macop_text(self._algo, f'Load of available pareto front backup from `{ self._filepath}`')
        else:           
            macop_text(self._algo, 'No pareto front found... Start running algorithm with new pareto front population.')
            logging.info("No pareto front backup used...")

"""UCB policy Checkpoint class implementation
"""

import os

class UCBCheckpoint(Callback):
    """
    UCB checkpoint is used for loading previous Upper Confidence Bound data and start again after loading checkpoint
    Need to be the same operators used during previous run (see `macop.policies.reinforcement.UCBPolicy` for more details)

    """
        """
        """

            logging.info("UCB Checkpoint is done into " + self._filepath)

                rewardsLine = ''

                for i, r in enumerate(self._algo._policy._rewards):
                    rewardsLine += str(r)

                    if i != len(self._algo._policy._rewards) - 1:
                        rewardsLine += ';'

                f.write(rewardsLine + '\n')

                occurrencesLine = ''

                for i, o in enumerate(self._algo._policy._occurences):
                    occurrencesLine += str(o)

                    if i != len(self._algo._policy._occurences) - 1:
                        occurrencesLine += ';'

                f.write(occurrencesLine + '\n')

        """
        Load backup lines as rewards and occurrences for UCB
        """

            logging.info('Load UCB data')

                lines = f.readlines()
                rewardsLine = lines[0].replace('\n', '')
                occurrencesLine = lines[1].replace('\n', '')

                self._algo._policy._rewards = [
                    float(f) for f in rewardsLine.split(';')
                ]
                self._algo._policy._occurences = [
                    float(f) for f in occurrencesLine.split(';')
                ]
            
            macop_text(self._algo, f'Load of available UCB policy data from `{self._filepath}`')
        else:
            macop_text(self._algo, 'No UCB data found, use default UCB policy')
            logging.info("No UCB data found...")

"""Abstract Evaluator class for computing fitness score associated to a solution

- stores into its `_data` dictionary attritute required measures when computing a solution
- `compute` abstract method enable to compute and associate a score to a given solution
"""

class Evaluator():
    """Abstract Evaluator class which enables to compute solution using specific `_data` 
    """
    def __init__(self, data):
        self._data = data

    def compute(self, solution):
        """Apply the computation of fitness from solution

        Fitness is a float value for mono-objective or set of float values if multi-objective evaluation

        Args:
            solution: {Solution} -- Solution instance

        Return:
            {float} -- computed solution score (float or set of float if multi-objective evaluation)
        """
        pass

        """Keep into evaluator reference of the whole algorithm
           The reason is to better manage evaluator instance if necessary

        Args:
            algo: {Algorithm} -- the algorithm reference runned
        """

"""Knapsack evaluators classes
"""
from .base import Evaluator

class KnapsackEvaluator(Evaluator):
    """Knapsack evaluator class which enables to compute solution using specific `_data`

    - stores into its `_data` dictionary attritute required measures when computing a knapsack solution
    - `_data['worths']` stores knapsack objects worths information
    - `compute` method enables to compute and associate a score to a given knapsack solution
    """

        Args:
    
        Returns:
            {float} -- fitness score of solution
        """
        fitness = 0
        for index, elem in enumerate(solution._data):
            fitness += self._data['worths'][index] * elem

        return fitness

"""Multi-objective evaluators classes 
"""

class WeightedSum(Evaluator):
    """Weighted-sum sub-evaluator class which enables to compute solution using specific `_data`

    - stores into its `_data` dictionary attritute required measures when computing a solution
    - `_data['evaluators']` current evaluator to use
    - `_data['weights']` Associated weight to use
    - `compute` method enables to compute and associate a tuples of scores to a given solution
    """

        - Associate tuple of fitness scores for each objective to the current solution
        - Compute weighted-sum for these objectives

        Args:
    
        Returns:
            {float} -- weighted-sum of the fitness scores
        """
        scores = [evaluator.compute(solution) for evaluator in self._data['evaluators']]

        # associate objectives scores to solution
        solution._scores = scores

        return sum([scores[i] for i, w in enumerate(self._data['weights'])])

"""Abstract Operator classes
"""
from enum import Enum

class KindOperator(Enum):
    """Enum in order to recognize kind of operators
    """
    MUTATOR = 1
    CROSSOVER = 2

class Operator():
    """Abstract Operator class which enables to update solution applying operator (computation)
    """
    def __init__(self):
        pass

    def apply(self, solution):
        """Apply the current operator transformation

        Args:
        """
        pass

        """Keep into operator reference of the whole algorithm
           The reason is to better manage operator instance

        Args:
        """

class Mutation(Operator):
    """Abstract Mutation extend from Operator

        kind: {KindOperator} -- specify the kind of operator
    """
        self._kind = KindOperator.MUTATOR

        raise NotImplementedError

class Crossover(Operator):
    """Abstract crossover extend from Operator

    """
        self._kind = KindOperator.CROSSOVER

"""Crossover implementations for discrete solutions kind
"""
import random
import sys

from ..base import Crossover

class SimpleCrossover(Crossover):
    """Crossover implementation which generated new solution by splitting at mean size best solution and current solution

        kind: {Algorithm} -- specify the kind of operator

    Example:

    >>> # operators import
    >>> from macop.operators.discrete.crossovers import SimpleCrossover
    >>> from macop.operators.discrete.mutators import SimpleMutation
    >>> # policy import
    >>> from macop.policies.reinforcement import UCBPolicy
    >>> # solution and algorithm
    >>> from macop.solutions.discrete import BinarySolution
    >>> from macop.algorithms.mono import IteratedLocalSearch
    >>> # evaluator import
    >>> from macop.evaluators.knapsacks import KnapsackEvaluator
    >>> # evaluator initialization (worths objects passed into data)
    >>> worths = [ random.randint(0, 20) for i in range(10) ]
    >>> evaluator = KnapsackEvaluator(data={'worths': worths})
    >>> # validator specification (based on weights of each objects)
    >>> weights = [ random.randint(20, 30) for i in range(10) ]
    >>> validator = lambda solution: True if sum([weights[i] for i, value in enumerate(solution._data) if value == 1]) < 200 else False
    >>> # initializer function with lambda function
    >>> initializer = lambda x=10: BinarySolution.random(x, validator)
    >>> # operators list with crossover and mutation
    >>> simple_crossover = SimpleCrossover()
    >>> simple_mutation = SimpleMutation()
    >>> operators = [simple_crossover, simple_mutation]
    >>> policy = UCBPolicy(operators)
    >>> algo = IteratedLocalSearch(initializer, evaluator, operators, policy, validator, maximise=True, verbose=False)
    >>> # using best solution, simple crossover is applied
    >>> best_solution = algo.run(100)
    >>> list(best_solution._data)
    [1, 1, 0, 1, 0, 1, 1, 1, 0, 1]
    >>> new_solution = initializer()
    >>> mutate_solution = simple_crossover.apply(new_solution)
    >>> list(mutate_solution._data)
    [0, 1, 0, 0, 0, 1, 1, 1, 0, 1]
    """
        """Create new solution based on best solution found and solution passed as parameter

        Args:
            solution: {Solution} -- the solution to use for generating new solution

        Returns:
            {Solution} -- new generated solution
        """

        size = solution._size

        # copy data of solution
        firstData = solution._data.copy()

        # get best solution from current algorithm
        copy_solution = self._algo._bestSolution.clone()

        splitIndex = int(size / 2)

        if random.uniform(0, 1) > 0.5:
            copy_solution._data[splitIndex:] = firstData[splitIndex:]
        else:
            copy_solution._data[:splitIndex] = firstData[:splitIndex]

        return copy_solution

class RandomSplitCrossover(Crossover):
    """Crossover implementation which generated new solution by randomly splitting best solution and current solution

    Example:

    >>> from macop.operators.discrete.crossovers import RandomSplitCrossover
    >>> random_split_crossover = RandomSplitCrossover()
    >>> operators = [random_split_crossover, simple_mutation]
    [1, 1, 1, 0, 1, 0, 1, 1, 1, 0]
    >>> mutate_solution = random_split_crossover.apply(new_solution)
    [1, 0, 0, 1, 1, 0, 0, 1, 0, 0]
    """

        Args:

        Returns:
        """

        splitIndex = random.randint(0, size)

        else:

"""Mutation implementations for discrete solution
"""
import sys

from ..base import Mutation

class SimpleMutation(Mutation):
    """Mutation implementation for binary solution, swap two bits randomly from solution

    Example:

    >>> # import of solution and simple mutation operator
    >>> solution = BinarySolution.random(5)
    >>> list(solution._data)
    [1, 0, 0, 0, 1]
    >>> mutator = SimpleMutation()
    >>> mutation_solution = mutator.apply(solution)
    >>> list(mutation_solution._data)
    [0, 0, 1, 0, 1]
    """
        """Create new solution based on solution passed as parameter

        Args:

        Returns:
        """

        firstCell = 0
        secondCell = 0

        # copy of solution
        copy_solution = solution.clone()

        while firstCell == secondCell:
            firstCell = random.randint(0, size - 1)
            secondCell = random.randint(0, size - 1)

        temp = copy_solution._data[firstCell]

        # swicth values
        copy_solution._data[firstCell] = copy_solution._data[secondCell]
        copy_solution._data[secondCell] = temp

class SimpleBinaryMutation(Mutation):
    """Mutation implementation for binary solution, swap bit randomly from solution

    Example:

    >>> # import of solution and simple binary mutation operator
    >>> from macop.operators.discrete.mutators import SimpleBinaryMutation
    [0, 1, 0, 0, 0]
    >>> mutator = SimpleBinaryMutation()
    [1, 1, 0, 0, 0]
    """

        Args:

        Returns:
        """

        cell = random.randint(0, size - 1)

        if copy_solution._data[cell]:
            copy_solution._data[cell] = 0
        else:
            copy_solution._data[cell] = 1

"""Abstract classes for Operator Selection Strategy
"""

# define policy to choose `operator` function at current iteration
class Policy():
    """Abstract class which is used for applying strategy when selecting and applying operator 

        operators: {[Operator]} -- list of selected operators for the algorithm
    """
    def __init__(self, operators):

    def select(self):
        """
        Select specific operator

        Returns:
            {Operator} -- selected operator
        """
        pass

        """
        Apply specific operator chosen to create new solution, computes its fitness and returns solution
        
        Args:
            _solution: {Solution} -- the solution to use for generating new solution

        Returns:
        """

        operator = self.select()

        logging.info("---- Applying %s on %s" %
                     (type(operator).__name__, solution))

        # apply operator on solution
        newSolution = operator.apply(solution)

        logging.info("---- Obtaining %s" % (solution))

        return newSolution

        """Keep into policy reference of the whole algorithm
           The reason is to better manage the operator choices (use of rewards as example)

        Args:
        """

"""Classical policies classes implementations
"""

from .base import Policy

class RandomPolicy(Policy):
    """Policy class implementation which is used for select operator randomly from the `operators` list

    Example:

    >>> import random
    >>> random.seed(42)
    >>> from macop.policies.classicals import RandomPolicy
    >>> policy = RandomPolicy([SimpleCrossover(), SimpleMutation()])
    >>> operator = policy.select()
    >>> type(operator).__name__
    'SimpleCrossover'
    """
        """Select randomly the next operator to use

        Returns:
            {Operator}: the selected operator

        """
        # choose operator randomly
        index = random.randint(0, len(self._operators) - 1)
        return self._operators[index]

"""Reinforcement learning policy classes implementations for Operator Selection Strategy
"""

class UCBPolicy(Policy):
    """Upper Confidence Bound (UCB) policy class which is used for applying UCB strategy when selecting and applying operator

    Rather than performing exploration by simply selecting an arbitrary action, chosen with a probability that remains constant,
    the UCB algorithm changes its exploration-exploitation balance as it gathers more knowledge of the environment.
    It moves from being primarily focused on exploration, when actions that have been tried the least are preferred,
    to instead concentrate on exploitation, selecting the action with the highest estimated reward.

    - Resource link: https://banditalgs.com/2016/09/18/the-upper-confidence-bound-algorithm/

        C: {float} -- The second half of the UCB equation adds exploration, with the degree of exploration being controlled by the hyper-parameter `C`.
        exp_rate: {float} -- exploration rate (probability to choose randomly next operator)
        rewards: {[float]} -- list of summed rewards obtained for each operator
        occurrences: {[int]} -- number of use (selected) of each operator

    Example:

    >>> worths = [ random.randint(0, 20) for i in range(20) ]
    >>> weights = [ random.randint(5, 30) for i in range(20) ]
    >>> initializer = lambda x=20: BinarySolution.random(x, validator)
    >>> operators = [SimpleCrossover(), SimpleMutation()]
    >>> policy._occurences
    [0, 0]
    >>> solution = algo.run(100)
    >>> type(solution).__name__
    'BinarySolution'
    >>> policy._occurences # one more due to first evaluation
    [51, 53]
    """
    def __init__(self, operators, C=100., exp_rate=0.5):
        self._rewards = [0. for o in self._operators]
        self._occurences = [0 for o in self._operators]
        self._C = C
        self._exp_rate = exp_rate

        """Select using Upper Confidence Bound the next operator to use (using acquired rewards)

        Returns:
        """

        indices = [i for i, o in enumerate(self._occurences) if o == 0]

        # random choice following exploration rate
        if np.random.uniform(0, 1) <= self._exp_rate:

            index = random.choice(range(len(self._operators)))
            return self._operators[index]

        elif len(indices) == 0:

            # if operator have at least be used one time
            ucbValues = []
            nVisits = sum(self._occurences)

            for i in range(len(self._operators)):

                ucbValue = self._rewards[i] + self._C * math.sqrt(
                    math.log(nVisits) / (self._occurences[i] + 0.1))
                ucbValues.append(ucbValue)

            return self._operators[ucbValues.index(max(ucbValues))]

        else:
            return self._operators[random.choice(indices)]

        """

        - fitness improvment is saved as rewards
        - selected operator occurence is also increased

        Args:

        Returns:
        """

        # compute fitness of new solution
        newSolution.evaluate(self._algo._evaluator)

        # compute fitness improvment rate
        if self._algo._maximise:
            fir = (newSolution.fitness() -
                   solution.fitness()) / solution.fitness()
        else:
            fir = (solution.fitness() -
                   newSolution.fitness()) / solution.fitness()

        operator_index = self._operators.index(operator)

        if fir > 0:
            self._rewards[operator_index] += fir

        self._occurences[operator_index] += 1

"""Abstract solution class
"""

from copy import deepcopy

class Solution():
    """Base abstract solution class structure
    
    - stores solution data representation into `data` attribute
    - get size (shape) of specific data representation
    - stores the score of the solution
    """

    def __init__(self, data, size):
        """
        Abstract solution class constructor

        Attributes:
            data: {ndarray} --  array of binary values
            size: {int} -- size of binary array values
            score: {float} -- fitness score value
        """
        self._size = size
        self._score = None

    def isValid(self, validator):
        """
        Use of custom method which validates if solution is valid or not

        Args:
            validator: {function} -- specific function which validates or not a solution

        Returns:
            {bool} -- `True` is solution is valid
        """
        return validator(self)

    def evaluate(self, evaluator):
        """
        Evaluate solution using specific `evaluator`

        Args:
            _evaluator: {function} -- specific function which computes fitness of solution

        Returns:
            {float} -- fitness score value
        """
        self._score = evaluator.compute(self)
        return self._score

    def fitness(self):
        """
        Returns fitness score

        Returns:
        """

    @staticmethod
    def random(size, validator=None):
        """
        Initialize solution using random data with validator

        Args:
            size: {int} -- expected solution size to generate
            validator: {function} -- specific function which validates or not a solution (if None, not validation is applied)

        Returns:
            {Solution} -- generated solution
        """
        return None

    def clone(self):
        """Clone the current solution and its data, but without keeping evaluated `_score`

        Returns:
            {Solution} -- clone of current solution
        """
        copy_solution = deepcopy(self)
        copy_solution._score = None

        print("Generic solution with ", self._data)

"""Discrete solution classes implementations
"""

# modules imports
from .base import Solution

class BinarySolution(Solution):
    """
    Binary integer solution class

    - store solution as a binary array (example: [0, 1, 0, 1, 1])
    - associated size is the size of the array
    - mainly use for selecting or not an element in a list of valuable objects

        data: {ndarray} --  array of binary values
        size: {int} -- size of binary array values
        score: {float} -- fitness score value
    """
        """
        Initialize binary solution using specific data

        Args:

        Example:

        >>> from macop.solutions.discrete import BinarySolution
        >>> # build of a solution using specific data and size
        >>> data = [0, 1, 0, 1, 1]
        >>> solution = BinarySolution(data, len(data))
        >>> # check data content
        >>> sum(solution._data) == 3
        True
        >>> # clone solution
        >>> solution_copy = solution.clone()
        >>> all(solution_copy._data == solution._data)
        True
        """
        super().__init__(np.array(data), size)

        """
        Intialize binary array with use of validator to generate valid random solution

        Args:

        Returns:
            {BinarySolution} -- new generated binary solution

        Example:

        >>> validator = lambda solution: True if sum(solution._data) > 5 else False
        >>> solution = BinarySolution.random(10, validator)
        >>> sum(solution._data) > 5
        True
        """

        data = np.random.randint(2, size=size)
        solution = BinarySolution(data, size)

        if not validator:

        while not validator(solution):
            data = np.random.randint(2, size=size)
            solution = BinarySolution(data, size)

        return solution

        return f"Binary solution {self._data}"

class CombinatoryIntegerSolution(Solution):
    """
    Combinatory integer solution class

        data: {ndarray} --  array of integer values
        size: {int} -- size of integer array values
    """
        """
        Initialize integer solution using specific data

        Args:
            data: {ndarray} --  array of integer values
            size: {int} -- size of integer array values

        >>> from macop.solutions.discrete import CombinatoryIntegerSolution
        >>> import numpy as np
        >>> data = np.arange(5)
        >>> solution = CombinatoryIntegerSolution(data, 5)
        >>> sum(solution._data) == 10
        True
        True
        """
        super().__init__(data, size)

        """
        Intialize combinatory integer array with use of validator to generate valid random solution

        Args:

        Returns:
            {CombinatoryIntegerSolution} -- new generated combinatory integer solution

        Example:

        >>> solution = CombinatoryIntegerSolution.random(5, validator)
        True
        """

        data = np.arange(size)
        np.random.shuffle(data)
        solution = CombinatoryIntegerSolution(data, size)

            data = np.arange(size)
            np.random.shuffle(data)
            solution = CombinatoryIntegerSolution(data, size)

        return f"Combinatory integer solution {self._data}"

class IntegerSolution(Solution):
    """
    Integer solution class

    """
        """

        Args:

        Example:

        >>> from macop.solutions.discrete import IntegerSolution
        >>> np.random.seed(42)
        >>> data = np.random.randint(5, size=10)
        >>> solution = IntegerSolution(data, 10)
        >>> sum(solution._data)
        28
        True
        """

        """
        Intialize integer array with use of validator to generate valid random solution

        Args:

        Returns:
            {IntegerSolution} -- new generated integer solution

        Example:

        >>> solution = IntegerSolution.random(5, validator)
        >>> sum(solution._data) > 10
        True
        """

        data = np.random.randint(size, size=size)
        solution = IntegerSolution(data, size)

            data = np.random.randint(size, size=size)
            solution = IntegerSolution(data, size)

        return f"Integer solution {self._data}"

"""Utils progress `macop` module when verbose enable
"""
import sys

class Colors:
    """Macop color representation
    """
    ENDC = '\033[m'
    GREEN = '\033[32m'
    GREY = '\033[90m'

def macop_text(algo, msg):
    """Display Macop message to user interface

    Args:
        algo: {Algorithm} -- current algorithm instance
        msg: {str} -- message to display
    """
    if algo._verbose:
        print(Colors.GREEN + 'M' + Colors.ENDC + Colors.GREY + 'acop' \
        + Colors.ENDC + Colors.GREEN + ' :: ' + Colors.ENDC \
        + Colors.GREY + msg + Colors.ENDC)

def macop_line(algo):
    """Macop split line

    Args:
    """

    if not algo._verbose:
        return

    line = ''

    for i in range(41):

        if i % 2 == 0:
            line += Colors.GREEN + '----' + Colors.ENDC
        else:
            line += Colors.GREY + '----' + Colors.ENDC

    print(line)

def macop_progress(algo, evaluations, max):
    """Progress line of macop

    Args:
        evaluations: {int} -- current number of evaluations
        max: {int} -- max number of expected evaluations
    """
        return

    barWidth = 156

    progress = evaluations / float(max)

    output_str = Colors.GREEN + '[' + Colors.ENDC
    pos = int(barWidth * progress)
    for i in range(barWidth):
        if i < pos:
            output_str = output_str + Colors.GREY + '=' + Colors.ENDC
        elif i == pos:
            output_str = output_str + Colors.GREEN + '>' + Colors.ENDC
        else:
            output_str = output_str + Colors.GREY + ' ' + Colors.ENDC

    output_str = output_str + Colors.GREEN + '] ' + Colors.ENDC + str(
        int(progress * 100.0)) + "%\r"
    print(output_str)
    sys.stdout.write("\033[F")

    # go to line
    if progress >= 1.:
        print()
        print(macop_line(algo))