Parcourir la source

Merge branch 'release/v1.0.15'

Jérôme BUISINE il y a 3 ans
Parent
commit
277bcb1a72

+ 10 - 0
README.md

@@ -39,6 +39,16 @@ Flexible discrete optimisation package allowing a quick implementation of your p
 - **Extensible:** the package is open to extension, i.e. it does not partition the user in these developer choices. It can just as well implement continuous optimization problems if needed while making use of the main interaction loop proposed by the package.
 - **Easy Setup:** as a pure Python package distributed is ``pip`` installable and easy to use.
 
+## Target Audience 
+
+This package would meet the expectations of people wishing to: 
+- Solve a complex problem oriented evolutionary algorithm but who do not wish to develop their own framework. They can rely on what the package already proposes but also on its generic and flexible contribution in order to adapt their own content;
+- Conduct research work leading to the rapid modification of meta-heuristics and the interaction of different algorithms. More precisely:
+  - test new combinations of algorithms. Changing algorithms during evaluations, e.g. different local searches;
+  - provide reinforcement learning during searches (e.g. adaptive operator choice strategy).
+  - test new multi-objective methods quickly thanks to the proposed algorithmic hierarchy allowing to easily decompose the multi-objective problem into single-objective sub-problems.
+- Take advantage of a system for launching calculations from a backup in order to avoid any loss in case of unwanted program interruption;
+- Quickly model a problem that is still unknown, i.e. the type of solution and the evaluation function, while taking advantage of the interaction loop proposed by the package.
 
 ## Content
 

+ 3 - 2
docs/source/api.rst

@@ -122,7 +122,7 @@ macop.solutions
 Continuous Optimisation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Although continuous optimization is not the priority of this package, the idea is to leave the possibility to any user to implement or even propose implementations related to this kind of problem. The modules are here for the moment empty but present to establish the structure relative to these types of implementations.
+Although continuous optimization is not the priority of this package, the idea is to leave the possibility to any user to implement or even propose implementations related to this kind of problem. The modules are here for the moment nearly empty (only with Zdt functions example) but present to establish the structure relative to these types of implementations.
 
 If a user wishes to propose these developments so that they can be added in a future version of the package, he can refer to the guidelines_ for contributions of the package.
 
@@ -134,7 +134,8 @@ macop.evaluators
 .. autosummary::
    :toctree: macop
    
-   macop.evaluators.continuous
+   macop.evaluators.continuous.mono
+   macop.evaluators.continuous.multi
 
 macop.operators
 -------------------

+ 2 - 2
docs/source/conf.py

@@ -25,9 +25,9 @@ copyright = '2021, Jérôme BUISINE'
 author = 'Jérôme BUISINE'
 
 # The short X.Y version
-version = '1.0.14'
+version = '1.0.15'
 # The full version, including alpha/beta/rc tags
-release = 'v1.0.14'
+release = 'v1.0.15'
 
 
 # -- General configuration ---------------------------------------------------

+ 14 - 0
docs/source/description.rst

@@ -30,6 +30,20 @@ Flexible discrete optimisation package allowing a quick implementation of your p
 - **Easy Setup:** as a pure Python package distributed is ``pip`` installable and easy to use.
 
 
+Target Audience
+~~~~~~~~~~~~~~~
+
+This package would meet the expectations of people wishing to: 
+
+- Solve a complex problem oriented evolutionary algorithm but who do not wish to develop their own framework. They can rely on what the package already proposes but also on its generic and flexible contribution in order to adapt their own content;
+- Conduct research work leading to the rapid modification of meta-heuristics and the interaction of different algorithms. More precisely:
+
+  - test new combinations of algorithms. Changing algorithms during evaluations, e.g. different local searches;
+  - provide reinforcement learning during searches (e.g. adaptive operator choice strategy).
+  - test new multi-objective methods quickly thanks to the proposed algorithmic hierarchy allowing to easily decompose the multi-objective problem into single-objective sub-problems.
+
+- Take advantage of a system for launching calculations from a backup in order to avoid any loss in case of unwanted program interruption;
+- Quickly model a problem that is still unknown, i.e. the type of solution and the evaluation function, while taking advantage of the interaction loop proposed by the package.
 
 Installation
 ------------

+ 50 - 339
docs/source/documentations.rst

@@ -100,84 +100,6 @@ Inside macop.solutions.base_ module of `Macop`, the ``Solution`` class is availa
 
 Some specific methods are available:
 
-.. code-block:: python
-
-    class Solution():
-
-        def __init__(self, data, size):
-            """
-            Abstract solution class constructor
-            """
-            ...
-
-        def isValid(self, validator):
-            """
-            Use of custom function which checks if a solution is valid or not
-            """
-            ...
-
-        def evaluate(self, evaluator):
-            """
-            Evaluate solution using specific `evaluator`
-            """
-            ...
-
-        @property
-        def fitness(self):
-            """
-            Returns fitness score (by default `score` private attribute)
-            """
-            ...
-
-        @fitness.setter
-        def fitness(self, score):
-            """
-            Set solution score as wished (by default `score` private attribute)
-            """
-            ...
-
-        @property
-        def data(self):
-            """
-            Returns solution data (by default `data` private attribute)
-            """
-            ...
-
-        @data.setter
-        def data(self, data):
-            """
-            Set solution data (by default `data` private attribute)
-            """
-            ...
-
-
-        @property
-        def size(self):
-            """
-            Returns solution size (by default `size` private attribute)
-            """
-            ...
-
-        @size.setter
-        def size(self, size):
-            """
-            Set solution size (by default `size` private attribute)
-            """
-            ...
-
-        @staticmethod
-        def random(size, validator=None):
-            """
-            initialise solution using random data with validator or not
-            """
-            ...
-
-        def clone(self):
-            """
-            Clone the current solution and its data, but without keeping evaluated `_score`
-            """
-            ...
-
 .. caution::
     An important thing here are the ``fitness``, ``size`` and ``data`` functions brought as an editable attribute by the ``@property`` and ``@XXXXX.setter`` decorators. The idea is to allow the user to modify these functions in order to change the expected result of the algorithm regardless of the data to be returned/modified. 
 
@@ -342,31 +264,9 @@ As for the management of solutions, a generic evaluator class macop.evaluators.b
 
 Abstract Evaluator class is used for computing fitness score associated to a solution. To evaluate all the solutions, this class:
 
-- stores into its ``_data`` dictionary attritute required measures when computing a solution
+- stores into its ``data`` initialiser dictionary attritute required measures when computing a solution
 - has a ``compute`` abstract method enable to compute and associate a score to a given solution
-- stores into its ``_algo`` attritute the current algorithm to use (we will talk about algorithm later)
-
-.. code-block: python
-
-    class Evaluator():
-    """
-    Abstract Evaluator class which enables to compute solution using specific `_data` 
-    """
-    def __init__(self, data):
-        self._data = data
-
-    @abstractmethod
-    def compute(self, solution):
-        """
-        Apply the computation of fitness from solution
-        """
-        pass
-
-    def setAlgo(self, algo):
-        """
-        Keep into evaluator reference of the whole algorithm
-        """
-        self._algo = algo
+- stores into its ``algo`` attritute the current algorithm to use (we will talk about algorithm later)
 
 We must therefore now create our own evaluator based on the proposed structure.
 
@@ -376,7 +276,7 @@ Custom evaluator
 To create our own evaluator, we need both:
 
 - data useful for evaluating a solution
-- calculate the score (fitness) associated with the state of the solution from these data. Hence, implement specific ``compute`` method.
+- compute the fitness associated with the state of the solution from these data. Hence, implement specific ``compute`` method.
 
 We will define the ``KnapsackEvaluator`` class, which will therefore allow us to evaluate solutions to our current problem.
 
@@ -389,7 +289,7 @@ We will define the ``KnapsackEvaluator`` class, which will therefore allow us to
 
     class KnapsackEvaluator(Evaluator):
         
-        def compute(solution):
+        def compute(self, solution):
 
             # `_data` contains worths array values of objects
             fitness = 0
@@ -443,60 +343,10 @@ In the discrete optimisation literature, we can categorise operators into two se
 
 Inside **Macop**, operators are also decomposed into these two categories. Inside macop.operators.base_, generic class ``Operator`` enables to manage any kind of operator.
 
-.. code-block:: python
-
-    class Operator():
-        """
-        Abstract Operator class which enables to update solution applying operator (computation)
-        """
-        @abstractmethod
-        def __init__(self):
-            pass
-
-        @abstractmethod
-        def apply(self, solution):
-            """
-            Apply the current operator transformation
-            """
-            pass
-
-        def setAlgo(self, algo):
-            """
-            Keep into operator reference of the whole algorithm
-            """
-            self._algo = algo
-
 Like the evaluator, the operator keeps **track of the algorithm** (using ``setAlgo`` method) to which he will be linked. This will allow better management of the way in which the operator must take into account the state of current data relating to the evolution of research.
 
 ``Mutation`` and ``Crossover`` classes inherite from ``Operator``. An ``apply`` function is required for any new operator.
 
-.. code-block:: python
-
-    class Mutation(Operator):
-        """Abstract Mutation extend from Operator
-
-        Attributes:
-            kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
-        """
-        def __init__(self):
-            self._kind = KindOperator.MUTATOR
-
-        def apply(self, solution):
-            raise NotImplementedError
-
-
-    class Crossover(Operator):
-        """Abstract crossover extend from Operator
-
-        Attributes:
-            kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
-        """
-        def __init__(self):
-            self._kind = KindOperator.CROSSOVER
-
-        def apply(self, solution1, solution2):
-            raise NotImplementedError
-
 We will now detail these categories of operators and suggest some relative to our problem.
 
 Mutator operator
@@ -597,7 +447,7 @@ The first half of solution 1 will be saved and added to the second half of solut
             splitIndex = int(size / 2)
 
             # copy data of solution 1
-            firstData = solution1._data.copy()
+            firstData = solution1.data.copy()
 
             # copy of solution 2
             copy_solution = solution2.clone()
@@ -656,36 +506,10 @@ Custom policy
 
 In our case, we are not going to exploit a complex enough implementation of a ``policy``. Simply, we will use a random choice of operator.
 
-First, let's take a look of the ``policy`` abstract class available in macop.policies.base_:
+First, let's take a look of the ``Policy`` abstract class available in macop.policies.base_:
 
-.. code-block:: python
-
-    class Policy():
-
-        def __init__(self, operators):
-            self._operators = operators
 
-        @abstractmethod
-        def select(self):
-            """
-            Select specific operator
-            """
-            pass
-
-        def apply(self, solution):
-            """
-            Apply specific operator to create new solution, compute its fitness and return it
-            """
-            ...
-
-        def setAlgo(self, algo):
-            """
-            Keep into policy reference of the whole algorithm
-            """
-            ...
-
-
-``Policy`` instance will have of ``_operators`` attributs in order to keep track of possible operators when selecting one. 
+``Policy`` instance will have of ``operators`` attributes in order to keep track of possible operators when selecting one. 
 Here, in our implementation we only need to specify the ``select`` abstract method. The ``apply`` method will select the next operator and return the new solution.
 
 .. code-block:: python
@@ -702,8 +526,8 @@ Here, in our implementation we only need to specify the ``select`` abstract meth
             Select specific operator
             """
             # choose operator randomly
-            index = random.randint(0, len(self._operators) - 1)
-            return self._operators[index]
+            index = random.randint(0, len(self.operators) - 1)
+            return self.operators[index]
 
 
 We can now use this operator choice policy to update our current solution:
@@ -784,121 +608,6 @@ She is composed of few default attributes:
 - callbacks: {[:class:`~macop.callbacks.base.Callback`]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initialising algorithm
 - parent: {:class:`~macop.algorithms.base.Algorithm`} -- parent algorithm reference in case of inner Algorithm instance (optional)
 
-.. code-block:: python
-
-    class Algorithm():
-
-        def __init__(self,
-                    initialiser,
-                    evaluator,
-                    operators,
-                    policy,
-                    validator,
-                    maximise=True,
-                    parent=None,
-                    verbose=True):
-            ...
-
-        def addCallback(self, callback):
-            """
-            Add new callback to algorithm specifying usefull parameters
-            """
-            ...
-
-        def resume(self):
-            """
-            Resume algorithm using Callback instances
-            """
-            ...
-
-        @property
-        def result(self):
-            """Get the expected result of the current algorithm
-
-            By default the best solution (but can be anything you want)
-            """
-            ...
-
-        @result.setter
-        def result(self, result):
-            """Set current default result of the algorithm
-            """
-            ...
-
-        def getParent(self):
-            """
-            Recursively find the main parent algorithm attached of the current algorithm
-            """
-            ...
-
-        def setParent(self, parent):
-            """
-            Set parent algorithm to current algorithm
-            """
-            ...
-
-        def initRun(self):
-            """
-            initialise the current solution and best solution using the `initialiser` function
-            """
-            ...
-
-        def increaseEvaluation(self):
-            """
-            Increase number of evaluation once a solution is evaluated for each dependant algorithm (parents hierarchy)
-            """
-            ...
-                
-        def getGlobalEvaluation(self):
-            """
-            Get the global number of evaluation (if inner algorithm)
-            """
-            ...
-
-        def getGlobalMaxEvaluation(self):
-            """
-            Get the global max number of evaluation (if inner algorithm)
-            """
-            ...
-
-        def stop(self):
-            """
-            Global stopping criteria (check for parents algorithm hierarchy too)
-            """
-            ...
-
-        def evaluate(self, solution):
-            """
-            Evaluate a solution using evaluator passed when intialize algorithm
-            """
-            ...
-
-        def update(self, solution):
-            """
-            Apply update function to solution using specific `policy`
-            Check if solution is valid after modification and returns it
-            """
-            ...
-
-        def isBetter(self, solution):
-            """
-            Check if solution is better than best found
-            """
-            ...
-
-        def run(self, evaluations):
-            """
-            Run the specific algorithm following number of evaluations to find optima
-            """
-            ...
-
-        def progress(self):
-            """
-            Log progress and apply callbacks if necessary
-            """
-            ...
-
-
 .. caution::
     An important thing here are the ``result`` functions brought as an editable attribute by the ``@property`` and ``@result.setter`` decorators. The idea is to allow the user to modify these functions in order to change the expected result of the algorithm regardless of the data to be returned/modified. 
 
@@ -1171,34 +880,6 @@ For this, the functionality relating to callbacks has been developed.
 Within **Macop**, a callback is a specific instance of macop.callbacks.base.Callback_ that allows you to perform an action of tracing / saving information **every** ``n`` **evaluations** but also reloading information if necessary when restarting an algorithm.
 
 
-.. code-block:: python
-
-    class Callback():
-
-        def __init__(self, every, filepath):
-            ...
-
-        @abstractmethod
-        def run(self):
-            """
-            Check if necessary to do backup based on `every` variable
-            """
-            pass
-
-        @abstractmethod
-        def load(self):
-            """
-            Load last backup line of solution and set algorithm state at this backup
-            """
-            pass
-
-        def setAlgo(self, algo):
-            """
-            Specify the main algorithm instance reference
-            """
-            ...
-
-
 - The ``run`` method will be called during run process of the algo and do backup at each specific number of evaluations. 
 - The ``load`` method will be used to reload the state of the algorithm from the last information saved. All saved data is saved in a file whose name will be specified by the user.
 
@@ -1222,9 +903,9 @@ We are going to create our own Callback instance called ``BasicCheckpoint`` whic
             Check if necessary to do backup based on `every` variable
             """
             # get current best solution
-            solution = self._algo._bestSolution
+            solution = self.algo._bestSolution
 
-            currentEvaluation = self._algo.getGlobalEvaluation()
+            currentEvaluation = self.algo.getGlobalEvaluation()
 
             # backup if necessary every number of evaluations
             if currentEvaluation % self._every == 0:
@@ -1267,21 +948,21 @@ We are going to create our own Callback instance called ``BasicCheckpoint`` whic
                     globalEvaluation = int(data[0])
 
                     # restore number of evaluations
-                    if self._algo.getParent() is not None:
-                        self._algo.getParent()._numberOfEvaluations = globalEvaluation
+                    if self.algo.getParent() is not None:
+                        self.algo.getParent()._numberOfEvaluations = globalEvaluation
                     else:
-                        self._algo._numberOfEvaluations = globalEvaluation
+                        self.algo._numberOfEvaluations = globalEvaluation
 
                     # get best solution data information
                     solution.data = list(map(int, data[1].split(' ')))
 
                     # avoid uninitialised solution
-                    if self._algo._bestSolution is None:
-                        self._algo._bestSolution = self._algo.initialiser()
+                    if self.algo._bestSolution is None:
+                        self.algo._bestSolution = self.algo.initialiser()
 
                     # set to algorithm the lastest obtained best solution
-                    self._algo._bestsolution.data = np.array(solution.data)
-                    self._algo._bestSolution._score = float(data[2])
+                    self.algo._bestsolution.data = np.array(solution.data)
+                    self.algo._bestSolution._score = float(data[2])
 
 
 In this way, it is possible to specify the use of a callback to our algorithm instance:
@@ -1363,9 +1044,12 @@ If we want to exploit this functionality, then we will need to exploit them with
 All the features of **Macop** were presented. The next section will aim to quickly present the few implementations proposed within **Macop** to highlight the modulality of the package.
 
 
-Implementation examples
+Advanced usages
 =======================
 
+Multi-objective discrete optimisation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 Within the API of **Macop**, you can find an implementation of The Multi-objective evolutionary algorithm based on decomposition (MOEA/D) is a general-purpose algorithm for approximating the Pareto set of multi-objective optimization problems. 
 It decomposes the original multi-objective problem into a number of single-objective optimization sub-problems and then uses an evolutionary process to optimize these sub-problems simultaneously and cooperatively. 
 MOEA/D is a state-of-art algorithm in aggregation-based approaches for multi-objective optimization.
@@ -1384,7 +1068,22 @@ An example with MOEAD for knapsack problem is available in knapsackMultiExample.
 
 .. _knapsackMultiExample.py: https://github.com/jbuisine/macop/blob/master/examples/knapsackMultiExample.py
 
+Continuous Zdt problems
+~~~~~~~~~~~~~~~~~~~~~~~
 
+Even if the package is not primarily intended for continuous optimisation, it allows for adaptation to continuous optimisation. 
+
+Based on the Zdt_ benchmarks function, it offers an implementation of Solution, Operator and Evaluator to enable the optimisation of this kind of problem.
+
+.. _Zdt: https://en.wikipedia.org/wiki/Test_functions_for_optimization
+
+- macop.solutions.continuous.ContinuousSolution_: manage float array solution in order to represent continuous solution;
+- macop.operators.continuous.mutators.PolynomialMutation_: update solution using polynomial mutation over solution's data;
+- macop.operators.continuous.crossovers.BasicDifferentialEvolutionCrossover_: use of new generated solutions in order to obtain new offspring solution;
+- macop.evaluators.continous.mono.ZdtEvaluator_: continuous evaluator for `Zdt` problem instance. Take into its ``data``, the ``f`` Zdt function;
+- macop.callbacks.classicals.ContinuousCallback_: manage callback and backup of continuous solution.
+
+A complete implementation example with the Rosenbrock_ function is available.
 
 .. _macop.algorithms.base: macop/macop.algorithms.base.html#module-macop.algorithms.base
 .. _macop.algorithms.mono: macop/macop.algorithms.mono.html#module-macop.algorithms.mono
@@ -1406,4 +1105,16 @@ An example with MOEAD for knapsack problem is available in knapsackMultiExample.
 .. _macop.callbacks.base.Callback: macop/macop.callbacks.base.html#macop.callbacks.base.Callback
 
 .. _macop.algorithms.multi.MOSubProblem: macop/macop.algorithms.multi.html#macop.algorithms.multi.MOSubProblem
-.. _macop.algorithms.multi.MOEAD: macop/macop.algorithms.multi.html#macop.algorithms.multi.MOEAD
+.. _macop.algorithms.multi.MOEAD: macop/macop.algorithms.multi.html#macop.algorithms.multi.MOEAD
+
+.. _macop.solutions.continuous.ContinuousSolution: macop/macop.solutions.continuous.html#macop.solutions.continuous.ContinuousSolution
+
+.. _macop.operators.continuous.mutators.PolynomialMutation: macop/macop.operators.continuous.mutators.html#macop.operators.continuous.mutators.PolynomialMutation
+.. _macop.operators.continuous.crossovers.BasicDifferentialEvolutionCrossover: macop/macop.operators.continuous.crossovers.html#macop.operators.continuous.crossovers.BasicDifferentialEvolutionCrossover
+
+.. _macop.evaluators.continous.mono.ZdtEvaluator: macop/macop.evaluators.continuous.mono.html#macop.evaluators.continous.mono.ZdtEvaluator
+
+.. _macop.callbacks.classicals.ContinuousCallback: macop/macop.callbacks.classicals.html#macop.callbacks.classicals.ContinuousCallback
+
+
+.. _Rosenbrock: https://github.com/jbuisine/macop/blob/master/examples/ZdtExample.py

+ 9 - 9
docs/source/documentations/callbacks.rst

@@ -92,9 +92,9 @@ We are going to create our own Callback instance called ``BasicCheckpoint`` whic
             Check if necessary to do backup based on `every` variable
             """
             # get current best solution
-            solution = self._algo._bestSolution
+            solution = self.algo._bestSolution
 
-            currentEvaluation = self._algo.getGlobalEvaluation()
+            currentEvaluation = self.algo.getGlobalEvaluation()
 
             # backup if necessary every number of evaluations
             if currentEvaluation % self._every == 0:
@@ -137,21 +137,21 @@ We are going to create our own Callback instance called ``BasicCheckpoint`` whic
                     globalEvaluation = int(data[0])
 
                     # restore number of evaluations
-                    if self._algo.getParent() is not None:
-                        self._algo.getParent()._numberOfEvaluations = globalEvaluation
+                    if self.algo.getParent() is not None:
+                        self.algo.getParent()._numberOfEvaluations = globalEvaluation
                     else:
-                        self._algo._numberOfEvaluations = globalEvaluation
+                        self.algo._numberOfEvaluations = globalEvaluation
 
                     # get best solution data information
                     solution.data = list(map(int, data[1].split(' ')))
 
                     # avoid uninitialised solution
-                    if self._algo._bestSolution is None:
-                        self._algo._bestSolution = self._algo.initialiser()
+                    if self.algo._bestSolution is None:
+                        self.algo._bestSolution = self.algo.initialiser()
 
                     # set to algorithm the lastest obtained best solution
-                    self._algo._bestsolution.getdata = ) = np.array(solution.data)
-                    self._algo._bestSolution._score = float(data[2])
+                    self.algo._bestsolution.getdata = ) = np.array(solution.data)
+                    self.algo._bestSolution._score = float(data[2])
 
 
 In this way, it is possible to specify the use of a callback to our algorithm instance:

+ 2 - 2
docs/source/documentations/evaluators.rst

@@ -12,7 +12,7 @@ Abstract Evaluator class is used for computing fitness score associated to a sol
 
 - stores into its ``_data`` dictionary attritute required measures when computing a solution
 - has a ``compute`` abstract method enable to compute and associate a score to a given solution
-- stores into its ``_algo`` attritute the current algorithm to use (we will talk about algorithm later)
+- stores into its ``algo`` attritute the current algorithm to use (we will talk about algorithm later)
 
 .. code-block: python
 
@@ -34,7 +34,7 @@ Abstract Evaluator class is used for computing fitness score associated to a sol
         """
         Keep into evaluator reference of the whole algorithm
         """
-        self._algo = algo
+        self.algo = algo
 
 We must therefore now create our own evaluator based on the proposed structure.
 

+ 1 - 1
docs/source/documentations/operators.rst

@@ -34,7 +34,7 @@ Inside **Macop**, operators are also decomposed into these two categories. Insid
             """
             Keep into operator reference of the whole algorithm
             """
-            self._algo = algo
+            self.algo = algo
 
 Like the evaluator, the operator keeps **track of the algorithm** (using ``setAlgo`` method) to which he will be linked. This will allow better management of the way in which the operator must take into account the state of current data relating to the evolution of research.
 

+ 4 - 4
docs/source/documentations/policies.rst

@@ -32,7 +32,7 @@ First, let's take a look of the ``policy`` abstract class available in ``macop.p
     class Policy():
 
         def __init__(self, operators):
-            self._operators = operators
+            self.operators = operators
 
         @abstractmethod
         def select(self):
@@ -54,7 +54,7 @@ First, let's take a look of the ``policy`` abstract class available in ``macop.p
             ...
 
 
-``Policy`` instance will have of ``_operators`` attributs in order to keep track of possible operators when selecting one. 
+``Policy`` instance will have of ``operators`` attributs in order to keep track of possible operators when selecting one. 
 Here, in our implementation we only need to specify the ``select`` abstract method. The ``apply`` method will select the next operator and return the new solution.
 
 .. code-block:: python
@@ -71,8 +71,8 @@ Here, in our implementation we only need to specify the ``select`` abstract meth
             Select specific operator
             """
             # choose operator randomly
-            index = random.randint(0, len(self._operators) - 1)
-            return self._operators[index]
+            index = random.randint(0, len(self.operators) - 1)
+            return self.operators[index]
 
 
 We can now use this operator choice policy to update our current solution:

+ 22 - 0
docs/source/macop/macop.evaluators.continuous.mono.rst

@@ -0,0 +1,22 @@
+macop.evaluators.continuous.mono
+================================
+
+.. automodule:: macop.evaluators.continuous.mono
+
+   
+   
+   
+
+   
+   
+   .. rubric:: Classes
+
+   .. autosummary::
+   
+      ZdtEvaluator
+   
+   
+
+   
+   
+   

+ 16 - 0
docs/source/macop/macop.evaluators.continuous.multi.rst

@@ -0,0 +1,16 @@
+macop.evaluators.continuous.multi
+=================================
+
+.. automodule:: macop.evaluators.continuous.multi
+
+   
+   
+   
+
+   
+   
+   
+
+   
+   
+   

+ 73 - 0
examples/ZdtExample.py

@@ -0,0 +1,73 @@
+# main imports
+import logging
+import os
+import random
+import numpy as np
+import math
+
+# module imports
+from macop.solutions.continuous import ContinuousSolution
+from macop.evaluators.continuous.mono import ZdtEvaluator
+
+from macop.operators.continuous.mutators import PolynomialMutation
+from macop.operators.continuous.crossovers import BasicDifferentialEvolutionCrossover
+
+from macop.policies.classicals import RandomPolicy
+
+from macop.algorithms.mono import IteratedLocalSearch as ILS
+from macop.algorithms.mono import HillClimberFirstImprovment
+from macop.callbacks.classicals import ContinuousCheckpoint
+
+if not os.path.exists('data'):
+    os.makedirs('data')
+
+# logging configuration
+logging.basicConfig(format='%(asctime)s %(message)s', filename='data/example.log', level=logging.DEBUG)
+
+random.seed(42)
+
+# usefull instance data
+n = 10
+filepath = "data/checkpoints_zdt_Rosenbrock.csv"
+problem_interval = -10, 10 # fixed value interval (avoid infinite)
+
+
+# check each value in order to validate
+def validator(solution):
+
+    mini, maxi = problem_interval
+
+    for x in solution.data:
+        if x < mini or x > maxi:
+            return False
+
+    return True
+
+# define init random solution
+def init():
+    return ContinuousSolution.random(n, problem_interval, validator)
+
+def main():
+
+    # Rosenbrock function with a=1 and b=100 (see https://en.wikipedia.org/wiki/Rosenbrock_function)
+    Rosenbrock_function = lambda s: sum([ 100 * math.pow(s.data[i + 1] - (math.pow(s.data[i], 2)), 2) + math.pow((1 - s.data[i]), 2) for i in range(len(s.data) - 1) ])
+
+    operators = [PolynomialMutation(interval=problem_interval), BasicDifferentialEvolutionCrossover(interval=problem_interval)]
+    policy = RandomPolicy(operators)
+    callback = ContinuousCheckpoint(every=5, filepath=filepath)
+    evaluator = ZdtEvaluator(data={'f': Rosenbrock_function})
+
+    # passing global evaluation param from ILS
+    hcfi = HillClimberFirstImprovment(init, evaluator, operators, policy, validator, maximise=False, verbose=True)
+    algo = ILS(init, evaluator, operators, policy, validator, localSearch=hcfi, maximise=False, verbose=True)
+    
+    # add callback into callback list
+    algo.addCallback(callback)
+
+    bestSol = algo.run(100000, ls_evaluations=100)
+    print(bestSol.data)
+
+    print('Solution for Rosenbrock Zdt instance score is {}'.format(evaluator.compute(bestSol)))
+
+if __name__ == "__main__":
+    main()

+ 2 - 0
macop/__init__.py

@@ -9,6 +9,8 @@ from macop.callbacks import multi
 from macop.evaluators import base
 from macop.evaluators.discrete import mono
 from macop.evaluators.discrete import multi
+from macop.evaluators.continuous import mono
+from macop.evaluators.continuous import multi
 
 from macop.operators import base
 from macop.operators.discrete import mutators

+ 19 - 19
macop/algorithms/base.py

@@ -63,7 +63,7 @@ class Algorithm():
         self.policy = policy
 
         # protected members intialization
-        self._operators = operators
+        self.operators = operators
         self._callbacks = []
         self._bestSolution = None
         self._currentSolution = None
@@ -81,7 +81,7 @@ class Algorithm():
         self._verbose = verbose
 
         # track reference of algorihtm into operator (keep an eye into best solution)
-        for operator in self._operators:
+        for operator in self.operators:
             if self._parent is not None:
                 operator.setAlgo(self.getParent())
             else:
@@ -123,13 +123,13 @@ class Algorithm():
             {:class:`~macop.algorithms.base.Algorithm`}: main algorithm set for this algorithm
         """
 
-        current_algorithm = self
+        currentalgorithm = 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
+        while currentalgorithm._parent is not None:
+            parent_alrogithm = currentalgorithm._parent
+            currentalgorithm = currentalgorithm._parent
 
         return parent_alrogithm
 
@@ -181,12 +181,12 @@ class Algorithm():
         Increase number of evaluation once a solution is evaluated for each dependant algorithm (parents hierarchy)
         """
 
-        current_algorithm = self
+        currentalgorithm = self
 
-        while current_algorithm is not None:
+        while currentalgorithm is not None:
 
-            current_algorithm._numberOfEvaluations += 1
-            current_algorithm = current_algorithm._parent
+            currentalgorithm._numberOfEvaluations += 1
+            currentalgorithm = currentalgorithm._parent
 
     def getGlobalEvaluation(self):
         """Get the global number of evaluation (if inner algorithm)
@@ -194,10 +194,10 @@ class Algorithm():
         Returns:
             {int}: current global number of evaluation
         """
-        parent_algorithm = self.getParent()
+        parentalgorithm = self.getParent()
 
-        if parent_algorithm is not None:
-            return parent_algorithm.getGlobalEvaluation()
+        if parentalgorithm is not None:
+            return parentalgorithm.getGlobalEvaluation()
 
         return self._numberOfEvaluations
 
@@ -224,10 +224,10 @@ class Algorithm():
             {int}: current global max number of evaluation
         """
 
-        parent_algorithm = self.getParent()
+        parentalgorithm = self.getParent()
 
-        if parent_algorithm is not None:
-            return parent_algorithm.getGlobalMaxEvaluation()
+        if parentalgorithm is not None:
+            return parentalgorithm.getGlobalMaxEvaluation()
 
         return self._maxEvaluations
 
@@ -235,11 +235,11 @@ class Algorithm():
         """
         Global stopping criteria (check for parents algorithm hierarchy too)
         """
-        parent_algorithm = self.getParent()
+        parentalgorithm = self.getParent()
 
         # based on global stopping creteria or on its own stopping critera
-        if parent_algorithm is not None:
-            return parent_algorithm._numberOfEvaluations >= parent_algorithm._maxEvaluations or self._numberOfEvaluations >= self._maxEvaluations
+        if parentalgorithm is not None:
+            return parentalgorithm._numberOfEvaluations >= parentalgorithm._maxEvaluations or self._numberOfEvaluations >= self._maxEvaluations
 
         return self._numberOfEvaluations >= self._maxEvaluations
 

+ 4 - 4
macop/algorithms/mono.py

@@ -343,16 +343,16 @@ class IteratedLocalSearch(Algorithm):
         # by default use of mother method to initialise variables
         super().run(evaluations)
 
+        # add same callbacks
+        for callback in self._callbacks:
+            self._localSearch.addCallback(callback)
+
         # enable resuming for ILS
         self.resume()
 
         # initialise current solution
         self.initRun()
 
-        # add same callbacks
-        for callback in self._callbacks:
-            self._localSearch.addCallback(callback)
-
         # local search algorithm implementation
         while not self.stop():
 

+ 3 - 3
macop/algorithms/multi.py

@@ -108,7 +108,7 @@ class MOEAD(Algorithm):
         self.evaluator = evaluator
         self.validator = validator
 
-        self._operators = operators
+        self.operators = operators
         self.policy = policy
         self._callbacks = []
 
@@ -126,7 +126,7 @@ class MOEAD(Algorithm):
         self._verbose = verbose
 
         # track reference of algo into operator (keep an eye into best solution)
-        for operator in self._operators:
+        for operator in self.operators:
             operator.setAlgo(self)
 
         # by default track reference for policy
@@ -543,7 +543,7 @@ class MOSubProblem(Algorithm):
             self.initRun()
 
         # new operators list keep track of current sub problem
-        for op in self._operators:
+        for op in self.operators:
             op.setAlgo(self)
 
         for _ in range(evaluations):

+ 2 - 2
macop/callbacks/base.py

@@ -23,7 +23,7 @@ class Callback():
             filepath: {str} -- file path where checkpoints will be saved
         """
 
-        self._algo = None
+        self.algo = None
         self._every = every
         self._filepath = filepath
 
@@ -39,7 +39,7 @@ class Callback():
         Args:
             algo: {:class:`~macop.algorithms.base.Algorithm`} -- main algorithm instance reference
         """
-        self._algo = algo
+        self.algo = algo
 
     @abstractmethod
     def run(self):

+ 112 - 20
macop/callbacks/classicals.py

@@ -25,25 +25,25 @@ class BasicCheckpoint(Callback):
         Check if necessary to do backup based on `every` variable
         """
         # get current best solution
-        solution = self._algo.result
+        solution = self.algo.result
 
-        currentEvaluation = self._algo.getGlobalEvaluation()
+        currentEvaluation = self.algo.getGlobalEvaluation()
 
         # backup if necessary
         if currentEvaluation % self._every == 0:
 
             logging.info("Checkpoint is done into " + self._filepath)
 
-            solution.data = ""
+            solution_data = ""
             solutionSize = len(solution.data)
 
             for index, val in enumerate(solution.data):
-                solution.data += str(val)
+                solution_data += str(val)
 
                 if index < solutionSize - 1:
-                    solution.data += ' '
+                    solution_data += ' '
 
-            line = str(currentEvaluation) + ';' + solution.data + ';' + str(
+            line = str(currentEvaluation) + ';' + solution_data + ';' + str(
                 solution.fitness) + ';\n'
 
             # check if file exists
@@ -70,33 +70,125 @@ class BasicCheckpoint(Callback):
                 # get evaluation  information
                 globalEvaluation = int(data[0])
 
-                if self._algo.getParent() is not None:
-                    self._algo.getParent().setEvaluation(globalEvaluation)
+                if self.algo.getParent() is not None:
+                    self.algo.getParent().setEvaluation(globalEvaluation)
                 else:
-                    self._algo.setEvaluation(globalEvaluation)
+                    self.algo.setEvaluation(globalEvaluation)
 
                 # get best solution data information
-                solution.data = list(map(int, data[1].split(' ')))
+                solution_data = list(map(int, data[1].split(' ')))
 
-                if self._algo.result is None:
-                    self._algo.result(self._algo.initialiser())
+                if self.algo.result is None:
+                    self.algo.result = self.algo.initialiser()
 
-                self._algo.result.data = np.array(solution.data)
-                self._algo.result.fitness = float(data[2])
+                self.algo.result.data = np.array(solution_data)
+                self.algo.result.fitness = float(data[2])
 
-            macop_line(self._algo)
-            macop_text(self._algo,
+            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.getEvaluation()}.'
+                self.algo,
+                f'Restart algorithm from evaluation {self.algo.getEvaluation()}.'
             )
         else:
             macop_text(
-                self._algo,
+                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)
+        macop_line(self.algo)
+
+
+class ContinuousCheckpoint(Callback):
+    """
+    ContinuousCheckpoint is used for loading previous computations and start again after loading checkpoint (only continuous solution)
+
+    Attributes:
+        algo: {:class:`~macop.algorithms.base.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 run(self):
+        """
+        Check if necessary to do backup based on `every` variable
+        """
+        # get current best solution
+        solution = self.algo.result
+
+        currentEvaluation = self.algo.getGlobalEvaluation()
+
+        # backup if necessary
+        if currentEvaluation % self._every == 0:
+
+            logging.info("Checkpoint is done into " + self._filepath)
+
+            solution_data = ""
+            solutionSize = len(solution.data)
+
+            for index, val in enumerate(solution.data):
+                solution_data += str(val)
+
+                if index < solutionSize - 1:
+                    solution_data += ' '
+
+            line = str(currentEvaluation) + ';' + solution_data + ';' + 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:
+                    f.write(line)
+
+    def load(self):
+        """
+        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().setEvaluation(globalEvaluation)
+                else:
+                    self.algo.setEvaluation(globalEvaluation)
+
+                # get best solution data information
+                solution_data = list(map(float, data[1].split(' ')))
+
+                if self.algo.result is None:
+                    self.algo.result = self.algo.initialiser()
+
+                self.algo.result.data = np.array(solution_data)
+                self.algo.result.fitness = 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.getEvaluation()}.'
+            )
+        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)

+ 26 - 26
macop/callbacks/multi.py

@@ -25,9 +25,9 @@ class MultiCheckpoint(Callback):
         Check if necessary to do backup based on `every` variable
         """
         # get current population
-        population = self._algo.population
+        population = self.algo.population
 
-        currentEvaluation = self._algo.getGlobalEvaluation()
+        currentEvaluation = self.algo.getGlobalEvaluation()
 
         # backup if necessary
         if currentEvaluation % self._every == 0:
@@ -48,7 +48,7 @@ class MultiCheckpoint(Callback):
 
                     line = str(currentEvaluation) + ';'
 
-                    for i in range(len(self._algo.evaluator)):
+                    for i in range(len(self.algo.evaluator)):
                         line += str(solution.scores[i]) + ';'
 
                     line += solution_data + ';\n'
@@ -74,42 +74,42 @@ class MultiCheckpoint(Callback):
                         # get evaluation  information
                         globalEvaluation = int(data[0])
 
-                        if self._algo.getParent() is not None:
-                            self._algo.getParent(
+                        if self.algo.getParent() is not None:
+                            self.algo.getParent(
                             )._numberOfEvaluations = globalEvaluation
                         else:
-                            self._algo._numberOfEvaluations = globalEvaluation
+                            self.algo._numberOfEvaluations = globalEvaluation
 
-                    nObjectives = len(self._algo.evaluator)
+                    nObjectives = len(self.algo.evaluator)
                     scores = [float(s) for s in data[1:nObjectives + 1]]
 
                     # get best solution data information
                     current_data = list(map(int, data[-1].split(' ')))
 
                     # initialise and fill with data
-                    self._algo.population[i] = self._algo.initialiser()
-                    self._algo.population[i].data = np.array(current_data)
-                    self._algo.population[i].scores = scores
+                    self.algo.population[i] = self.algo.initialiser()
+                    self.algo.population[i].data = np.array(current_data)
+                    self.algo.population[i].scores = scores
 
-                    self._algo.result.append(self._algo.population[i])
+                    self.algo.result.append(self.algo.population[i])
 
-            macop_line(self._algo)
+            macop_line(self.algo)
             macop_text(
-                self._algo,
+                self.algo,
                 f'Load of available population from `{self._filepath}`')
             macop_text(
-                self._algo,
-                f'Restart algorithm from evaluation {self._algo._numberOfEvaluations}.'
+                self.algo,
+                f'Restart algorithm from evaluation {self.algo._numberOfEvaluations}.'
             )
         else:
             macop_text(
-                self._algo,
+                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)
+        macop_line(self.algo)
 
 
 class ParetoCheckpoint(Callback):
@@ -126,9 +126,9 @@ class ParetoCheckpoint(Callback):
         Check if necessary to do backup based on `every` variable
         """
         # get current population
-        pfPop = self._algo.result
+        pfPop = self.algo.result
 
-        currentEvaluation = self._algo.getGlobalEvaluation()
+        currentEvaluation = self.algo.getGlobalEvaluation()
 
         # backup if necessary
         if currentEvaluation % self._every == 0:
@@ -149,7 +149,7 @@ class ParetoCheckpoint(Callback):
 
                     line = ''
 
-                    for i in range(len(self._algo.evaluator)):
+                    for i in range(len(self.algo.evaluator)):
                         line += str(solution.scores[i]) + ';'
 
                     line += solution_data + ';\n'
@@ -170,24 +170,24 @@ class ParetoCheckpoint(Callback):
 
                     data = line.replace(';\n', '').split(';')
 
-                    nObjectives = len(self._algo.evaluator)
+                    nObjectives = len(self.algo.evaluator)
                     scores = [float(s) for s in data[0:nObjectives]]
 
                     # get best solution data information
                     current_data = list(map(int, data[-1].split(' ')))
 
-                    self._algo.result[i].data = np.array(current_data)
-                    self._algo.result[i].scores = scores
+                    self.algo.result[i].data = np.array(current_data)
+                    self.algo.result[i].scores = scores
 
             macop_text(
-                self._algo,
+                self.algo,
                 f'Load of available pareto front backup from `{ self._filepath}`'
             )
         else:
             macop_text(
-                self._algo,
+                self.algo,
                 'No pareto front found... Start running algorithm with new pareto front population.'
             )
             logging.info("No pareto front backup used...")
 
-        macop_line(self._algo)
+        macop_line(self.algo)

+ 10 - 10
macop/callbacks/policies.py

@@ -26,7 +26,7 @@ class UCBCheckpoint(Callback):
         Check if necessary to do backup based on `every` variable
         """
         # get current population
-        currentEvaluation = self._algo.getGlobalEvaluation()
+        currentEvaluation = self.algo.getGlobalEvaluation()
 
         # backup if necessary
         if currentEvaluation % self._every == 0:
@@ -37,20 +37,20 @@ class UCBCheckpoint(Callback):
 
                 rewardsLine = ''
 
-                for i, r in enumerate(self._algo.policy.rewards):
+                for i, r in enumerate(self.algo.policy.rewards):
                     rewardsLine += str(r)
 
-                    if i != len(self._algo.policy.rewards) - 1:
+                    if i != len(self.algo.policy.rewards) - 1:
                         rewardsLine += ';'
 
                 f.write(rewardsLine + '\n')
 
                 occurrencesLine = ''
 
-                for i, o in enumerate(self._algo.policy.occurences):
+                for i, o in enumerate(self.algo.policy.occurences):
                     occurrencesLine += str(o)
 
-                    if i != len(self._algo.policy.occurences) - 1:
+                    if i != len(self.algo.policy.occurences) - 1:
                         occurrencesLine += ';'
 
                 f.write(occurrencesLine + '\n')
@@ -69,18 +69,18 @@ class UCBCheckpoint(Callback):
                 rewardsLine = lines[0].replace('\n', '')
                 occurrencesLine = lines[1].replace('\n', '')
 
-                self._algo.policy.rewards = [
+                self.algo.policy.rewards = [
                     float(f) for f in rewardsLine.split(';')
                 ]
-                self._algo.policy.occurences = [
+                self.algo.policy.occurences = [
                     float(f) for f in occurrencesLine.split(';')
                 ]
 
             macop_text(
-                self._algo,
+                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')
+            macop_text(self.algo, 'No UCB data found, use default UCB policy')
             logging.info("No UCB data found...")
 
-        macop_line(self._algo)
+        macop_line(self.algo)

+ 1 - 1
macop/evaluators/base.py

@@ -38,4 +38,4 @@ class Evaluator():
         Args:
             algo: {:class:`~macop.algorithms.base.Algorithm`} -- the algorithm reference runned
         """
-        self._algo = algo
+        self.algo = algo

+ 44 - 0
macop/evaluators/continuous/mono.py

@@ -0,0 +1,44 @@
+"""Mono-objective evaluators classes for continuous problem
+"""
+# main imports
+from macop.evaluators.base import Evaluator
+
+
+class ZdtEvaluator(Evaluator):
+    """Generic Zdt evaluator class which enables to compute custom Zdt function for continuous problem
+
+    - stores into its `_data` dictionary attritute required measures when computing a knapsack solution
+    - `_data['f']` stores lambda Zdt function 
+    - `compute` method enables to compute and associate a score to a given knapsack solution
+
+    Example:
+
+    >>> import random
+    >>>
+    >>> # binary solution import
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>>
+    >>> # evaluator import
+    >>> from macop.evaluators.continuous.mono import ZdtEvaluator
+    >>> solution_data = [2, 3, 4, 1, 2, 3, 3]
+    >>> size = len(solution_data)
+    >>> solution = ContinuousSolution(solution_data, size)
+    >>>
+    >>> # evaluator initialization (Shere function)
+    >>> f_sphere = lambda s: sum([ x * x for x in s.data])
+    >>> evaluator = ZdtEvaluator(data={'f': f_sphere})
+    >>>
+    >>> # compute solution score
+    >>> evaluator.compute(solution)
+    45
+    """
+    def compute(self, solution):
+        """Apply the computation of fitness from solution
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- Solution instance
+    
+        Returns:
+            {float}: fitness score of solution
+        """
+        return self._data['f'](solution)

+ 4 - 0
macop/evaluators/continuous/multi.py

@@ -0,0 +1,4 @@
+"""Multi-objective evaluators classes for continuous problem
+"""
+# main imports
+from macop.evaluators.base import Evaluator

+ 1 - 1
macop/evaluators/discrete/mono.py

@@ -1,4 +1,4 @@
-"""Mono-objective evaluators classes
+"""Mono-objective evaluators classes for discrete problem
 """
 # main imports
 from macop.evaluators.base import Evaluator

+ 1 - 1
macop/evaluators/discrete/multi.py

@@ -1,4 +1,4 @@
-"""Multi-objective evaluators classes 
+"""Multi-objective evaluators classes for discrete problem
 """
 # main imports
 from macop.evaluators.base import Evaluator

+ 1 - 1
macop/operators/base.py

@@ -37,7 +37,7 @@ class Operator():
         Args:
             algo: {:class:`~macop.algorithms.base.Algorithm`} -- the algorithm reference runned
         """
-        self._algo = algo
+        self.algo = algo
 
 
 class Mutation(Operator):

+ 79 - 0
macop/operators/continuous/crossovers.py

@@ -3,6 +3,85 @@
 # main imports
 import random
 import sys
+import numpy as np
 
 # module imports
 from macop.operators.base import Crossover
+
+class BasicDifferentialEvolutionCrossover(Crossover):
+    """Basic Differential Evolution implementation for continuous solution
+
+    Attributes:
+        kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
+
+    Example:
+
+    >>> # import of solution and polynomial mutation operator
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>> from macop.operators.continuous.crossovers import BasicDifferentialEvolutionCrossover
+    >>> solution = ContinuousSolution.random(5, (-2, 2))
+    >>> list(solution.data)
+    [-1.3760219186551894, -1.7676655513272022, 1.4647045830997407, 0.4044600469728352, 0.832290311184182]
+    >>> crossover = BasicDifferentialEvolutionCrossover(interval=(-2, 2))
+    >>> crossover_solution = crossover.apply(solution)
+    >>> list(crossover_solution.data)
+    [-1.7016619497704522, -0.43633033292228895, 2.0, -0.034751768954844, 0.6134819652022994]
+    """
+
+    def __init__(self, interval, CR=1.0, F=0.5):
+        """"Basic Differential Evolution crossover initialiser in order to specify kind of Operator and interval of continuous solution
+
+        Args:
+            interval: {(float, float)} -- minimum and maximum values interval of variables in the solution
+            CR: {float} -- probability to use of new generated solutions when modifying a value of current solution
+            F: {float} -- degree of impact of the new generated solutions on the current solution when obtaining new solution
+        """
+        super().__init__()
+
+        self.mini, self.maxi = interval
+        self.CR = CR
+        self.F = F
+
+
+    def apply(self, solution1, solution2=None):
+        """Create new solution based on solution passed as parameter
+
+        Args:
+            solution1: {:class:`~macop.solutions.base.Solution`} -- the first solution to use for generating new solution
+            solution2: {:class:`~macop.solutions.base.Solution`} -- the second solution to use for generating new solution
+
+        Returns:
+            {:class:`~macop.solutions.base.Solution`}: new continuous generated solution
+        """
+
+        size = solution1.size
+
+        solution1 = solution1.clone()
+
+        # create two new random solutions using instance and its static method
+        solution2 = solution1.random(size, interval=(self.mini, self.maxi))
+        solution3 = solution1.random(size, interval=(self.mini, self.maxi))
+
+        # apply crossover on the new computed solution
+        for i in range(len(solution1.data)):
+
+            # use of CR to change or not the current value of the solution using new solutions
+            if random.uniform(0, 1) < self.CR:
+                solution1.data[i] = solution1.data[i] + self.F * (solution2.data[i] - solution3.data[i])
+
+        # repair solution if necessary
+        solution1.data = self._repair(solution1)
+
+        return solution1
+
+    def _repair(self, solution):
+        """
+        Private repair function for solutions if an element is out of bounds of an expected interval 
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {ndarray} -- repaired array of float values
+        """
+        return np.array([self.mini if x < self.mini else self.maxi if x > self.maxi else x for x in solution.data])

+ 86 - 0
macop/operators/continuous/mutators.py

@@ -3,6 +3,92 @@
 # main imports
 import random
 import sys
+import numpy as np
 
 # module imports
 from macop.operators.base import Mutation
+
+class PolynomialMutation(Mutation):
+    """Polynomial Mutation implementation for continuous solution
+
+    Attributes:
+        kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
+
+    Example:
+
+    >>> # import of solution and polynomial mutation operator
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>> from macop.operators.continuous.mutators import PolynomialMutation
+    >>> solution = ContinuousSolution.random(5, (-2, 2))
+    >>> list(solution.data)
+    [-0.50183952461055, 1.8028572256396647, 0.9279757672456204, 0.3946339367881464, -1.375925438230254]
+    >>> mutator = PolynomialMutation(interval=(-2, 2))
+    >>> mutation_solution = mutator.apply(solution)
+    >>> list(mutation_solution.data)
+    [-0.50183952461055, 1.8028572256396647, 0.9279757672456204, 0.3946339367881464, -1.375925438230254]
+    """
+
+    def __init__(self, interval):
+        """Polynomial Mutation initialiser in order to specify kind of Operator and interval of continuous solution
+
+        Args:
+            interval: {(float, float)} -- minimum and maximum values interval of variables in the solution
+        """
+        super().__init__()
+
+        self.mini, self.maxi = interval
+
+
+    def apply(self, solution):
+        """Create new solution based on solution passed as parameter
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {:class:`~macop.solutions.base.Solution`}: new generated solution
+        """
+
+        size = solution.size
+        rate = float(1/size)
+
+        copy_solution = solution.clone()
+
+        rand = random.uniform(0, 1)
+
+        # apply mutation over the new computed solution
+        copy_solution.data = [x if rand > rate else x + self._sigma(size) * (self.maxi - (self.mini)) for x in solution.data]
+        copy_solution.data = self._repair(copy_solution)
+
+        return copy_solution
+
+    def _repair(self, solution):
+        """
+        Private repair function for solutions if an element is out of bounds of an expected interval
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {ndarray} -- repaired array of float values
+        """
+        return np.array([self.mini if x < self.mini else self.maxi if x > self.maxi else x for x in solution.data])
+
+    
+    def _sigma(self, size):
+        """
+        Compute the sigma value for polynomial mutation
+
+        Args:
+            size: {integer} -- 
+
+        Returns:
+            {float} -- expected sigma value depending on solution size
+        """
+        rand = random.uniform(0, 1)
+        sigma = 0
+        if rand < 0.5:
+            sigma = pow(2 * rand, 1 / (size + 1)) - 1
+        else:
+            sigma = 1 - pow(2 - 2 * rand, 1 / (size - 1))
+        return sigma

+ 3 - 3
macop/operators/discrete/crossovers.py

@@ -53,12 +53,12 @@ class SimpleCrossover(Crossover):
     >>> # using best solution, simple crossover is applied
     >>> best_solution = algo.run(100)
     >>> list(best_solution.data)
-    [1, 1, 0, 1, 1, 1, 1, 1, 0, 0]
+    [1, 1, 0, 1, 1, 1, 0, 1, 1, 1]
     >>> new_solution_1 = initialiser()
     >>> new_solution_2 = initialiser()
     >>> offspring_solution = simple_crossover.apply(new_solution_1, new_solution_2)
     >>> list(offspring_solution.data)
-    [0, 1, 0, 0, 0, 1, 1, 0, 1, 1]
+    [1, 0, 1, 1, 0, 0, 0, 1, 0, 0]
     """
     def apply(self, solution1, solution2=None):
         """Create new solution based on best solution found and solution passed as parameter
@@ -139,7 +139,7 @@ class RandomSplitCrossover(Crossover):
     >>> new_solution_2 = initialiser()
     >>> offspring_solution = random_split_crossover.apply(new_solution_1, new_solution_2)
     >>> list(offspring_solution.data)
-    [1, 0, 0, 1, 1, 1, 0, 0, 1, 1]
+    [0, 0, 0, 1, 1, 1, 1, 0, 0, 1]
     """
     def apply(self, solution1, solution2=None):
         """Create new solution based on best solution found and solution passed as parameter

+ 4 - 4
macop/policies/base.py

@@ -19,7 +19,7 @@ class Policy():
         Args:
             operators: [{}] -- list of operators to use
         """
-        self._operators = operators
+        self.operators = operators
 
     @abstractmethod
     def select(self):
@@ -49,8 +49,8 @@ class Policy():
                      (type(operator).__name__, solution1))
 
         # default value of solution2 is current best solution
-        if solution2 is None and self._algo is not None:
-            solution2 = self._algo.result
+        if solution2 is None and self.algo is not None:
+            solution2 = self.algo.result
 
         # avoid use of crossover if only one solution is passed
         if solution2 is None and operator._kind == KindOperator.CROSSOVER:
@@ -75,4 +75,4 @@ class Policy():
         Args:
             algo: {:class:`~macop.algorithms.base.Algorithm`} -- the algorithm reference runned
         """
-        self._algo = algo
+        self.algo = algo

+ 2 - 2
macop/policies/classicals.py

@@ -35,5 +35,5 @@ class RandomPolicy(Policy):
 
         """
         # choose operator randomly
-        index = random.randint(0, len(self._operators) - 1)
-        return self._operators[index]
+        index = random.randint(0, len(self.operators) - 1)
+        return self.operators[index]

+ 14 - 14
macop/policies/reinforcement.py

@@ -72,7 +72,7 @@ class UCBPolicy(Policy):
     >>> type(solution).__name__
     'BinarySolution'
     >>> policy.occurences # one more due to first evaluation
-    [53, 50]
+    [50, 52]
     """
     def __init__(self, operators, C=100., exp_rate=0.1):
         """UCB Policy initialiser
@@ -84,13 +84,13 @@ class UCBPolicy(Policy):
         """
 
         # private members
-        self._operators = operators
+        self.operators = operators
         self._C = C
         self._exp_rate = exp_rate
 
         # public members
-        self.rewards = [0. for o in self._operators]
-        self.occurences = [0 for o in self._operators]
+        self.rewards = [0. for o in self.operators]
+        self.occurences = [0 for o in self.operators]
 
     def select(self):
         """Select using Upper Confidence Bound the next operator to use (using acquired rewards)
@@ -104,8 +104,8 @@ class UCBPolicy(Policy):
         # 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]
+            index = random.choice(range(len(self.operators)))
+            return self.operators[index]
 
         elif len(indices) == 0:
 
@@ -113,16 +113,16 @@ class UCBPolicy(Policy):
             ucbValues = []
             nVisits = sum(self.occurences)
 
-            for i in range(len(self._operators)):
+            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))]
+            return self.operators[ucbValues.index(max(ucbValues))]
 
         else:
-            return self._operators[random.choice(indices)]
+            return self.operators[random.choice(indices)]
 
     def apply(self, solution1, solution2=None):
         """
@@ -145,8 +145,8 @@ class UCBPolicy(Policy):
                      (type(operator).__name__, solution1))
 
         # default value of solution2 is current best solution
-        if solution2 is None and self._algo is not None:
-            solution2 = self._algo.result
+        if solution2 is None and self.algo is not None:
+            solution2 = self.algo.result
 
         # avoid use of crossover if only one solution is passed
         if solution2 is None and operator._kind == KindOperator.CROSSOVER:
@@ -161,15 +161,15 @@ class UCBPolicy(Policy):
             newSolution = operator.apply(solution1)
 
         # compute fitness of new solution
-        newSolution.evaluate(self._algo.evaluator)
+        newSolution.evaluate(self.algo.evaluator)
 
         # compute fitness improvment rate
-        if self._algo._maximise:
+        if self.algo._maximise:
             fir = (newSolution.fitness - solution1.fitness) / solution1.fitness
         else:
             fir = (solution1.fitness - newSolution.fitness) / solution1.fitness
 
-        operator_index = self._operators.index(operator)
+        operator_index = self.operators.index(operator)
 
         if fir > 0:
             self.rewards[operator_index] += fir

+ 85 - 1
macop/solutions/continuous.py

@@ -1,2 +1,86 @@
 """Continuous solution classes implementation
-"""
+"""
+import numpy as np
+
+# modules imports
+from macop.solutions.base import Solution
+
+class ContinuousSolution(Solution):
+    """
+    Continuous solution class
+
+    - store solution as a float array (example: [0.5, 0.2, 0.17, 0.68, 0.42])
+    - associated size is the size of the array
+    - mainly use for selecting or not an element in a list of valuable objects
+
+    Attributes:
+        data: {ndarray} --  array of float values
+        size: {int} -- size of float array values
+        score: {float} -- fitness score value
+    """
+    def __init__(self, data, size):
+        """
+        initialise continuous solution using specific data
+
+        Args:
+            data: {ndarray} --  array of float values
+            size: {int} -- size of float array values
+
+        Example:
+
+        >>> from macop.solutions.continuous import ContinuousSolution
+        >>>
+        >>> # build of a solution using specific data and size
+        >>> data = [0.2, 0.4, 0.6, 0.8, 1]
+        >>> solution = ContinuousSolution(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)
+
+    @staticmethod
+    def random(size, interval, validator=None):
+        """
+        Intialize float array with use of validator to generate valid random solution
+
+        Args:
+            size: {int} -- expected solution size to generate
+            interval: {(float, float)} -- tuple with min and max expected interval value for current solution
+            validator: {function} -- specific function which validates or not a solution (if None, not validation is applied)
+
+        Returns:
+            {:class:`~macop.solutions.discrete.Continuous`}: new generated continuous solution
+
+        Example:
+
+        >>> from macop.solutions.continuous import ContinuousSolution
+        >>>
+        >>> # generate random solution using specific validator
+        >>> validator = lambda solution: True if sum(solution.data) > 5 else False
+        >>> solution = ContinuousSolution.random(10, (-2, 2), validator)
+        >>> sum(solution.data) > 5
+        True
+        """
+
+        mini, maxi = interval
+
+        data = np.random.random(size=size) * (maxi - mini) + mini
+        solution = ContinuousSolution(data, size)
+
+        if not validator:
+            return solution
+
+        while not validator(solution):
+            data = np.random.random(size=size) * (maxi - mini) + mini
+            solution = ContinuousSolution(data, size)
+
+        return solution
+
+    def __str__(self):
+        return f"Continuous solution {self._data}"

+ 11 - 0
paper.md

@@ -53,6 +53,17 @@ Hence, motivation behind **Macop** is a flexible discrete optimisation package a
 - **Extensible:** the package is open to extension, i.e. it does not partition the user in these developer choices. It can just as well implement continuous optimization problems if needed while making use of the main interaction loop proposed by the package.
 - **Easy Setup:** as a pure Python package distributed is `pip` installable and easy to use.
 
+# Target Audience 
+
+This package would meet the expectations of people wishing to: 
+- Solve a complex problem oriented evolutionary algorithm but who do not wish to develop their own framework. They can rely on what the package already proposes but also on its generic and flexible contribution in order to adapt their own content;
+- Conduct research work leading to the rapid modification of meta-heuristics and the interaction of different algorithms. More precisely:
+  - test new combinations of algorithms. Changing algorithms during evaluations, e.g. different local searches;
+  - provide reinforcement learning during searches (e.g. adaptive operator choice strategy).
+  - test new multi-objective methods quickly thanks to the proposed algorithmic hierarchy allowing to easily decompose the multi-objective problem into single-objective sub-problems.
+- Take advantage of a system for launching calculations from a backup in order to avoid any loss in case of unwanted program interruption;
+- Quickly model a problem that is still unknown, i.e. the type of solution and the evaluation function, while taking advantage of the interaction loop proposed by the package.
+
 # Description
 
 At the beginning of the development of this library, the idea of making it as modular as possible was topical. The library divide into sub-module forms considered to be the most important to build and solve an optimisation problem.

+ 10 - 1
setup.py

@@ -21,6 +21,9 @@ class TestCommand(distutils.command.check.check):
         from macop.operators.discrete import mutators as discrete_mutators
         from macop.operators.discrete import crossovers as discrete_crossovers
 
+        from macop.operators.continuous import mutators as continuous_mutators
+        from macop.operators.continuous import crossovers as continuous_crossovers
+
         # policies module
         from macop.policies import classicals
         from macop.policies import reinforcement
@@ -49,6 +52,12 @@ class TestCommand(distutils.command.check.check):
         doctest.testmod(discrete_mutators)
         doctest.testmod(discrete_crossovers)
 
+        random.seed(42)
+        np.random.seed(42)
+        # operators module
+        doctest.testmod(continuous_mutators)
+        doctest.testmod(continuous_crossovers)
+
         random.seed(42)
         np.random.seed(42)
         # policies module
@@ -73,7 +82,7 @@ class TestCommand(distutils.command.check.check):
 
 setup(
     name='macop',
-    version='1.0.14',
+    version='1.0.15',
     description='Minimalist And Customisable Optimisation Package',
     long_description=open('README.md').read(),
     long_description_content_type='text/markdown',