Repository: evopy/evopy

Issue:
Allow passing a reporting callback
Users should be able to get an indication of progress while the algorithm is running. I think a callback would be best, called after every evolution with the best individual and its fitness.

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
index 5ca1db6..dc4f4fb 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -551,7 +551,7 @@ max-returns=6
 max-statements=50
 
 # Minimum number of public methods for a class (see R0903).
-min-public-methods=1
+min-public-methods=0
 
 
 [EXCEPTIONS]
diff --git a/evopy/__init__.py b/evopy/__init__.py
index 30a793d..9d11fab 100644
--- a/evopy/__init__.py
+++ b/evopy/__init__.py
@@ -1,2 +1,3 @@
 """The evopy evolutionary strategy algorithm package."""
 from evopy.evopy import EvoPy
+from evopy.progress_report import ProgressReport
diff --git a/evopy/evopy.py b/evopy/evopy.py
index 27d5c5c..83d04db 100644
--- a/evopy/evopy.py
+++ b/evopy/evopy.py
@@ -2,13 +2,14 @@
 import numpy as np
 
 from evopy.individual import Individual
+from evopy.progress_report import ProgressReport
 
 
 class EvoPy:
     """Main class of the EvoPy package."""
 
     def __init__(self, fitness_function, individual_length, warm_start=None, generations=100,
-                 population_size=30, num_children=1, mean=0, std=1, maximize=False):
+                 population_size=30, num_children=1, mean=0, std=1, maximize=False, reporter=None):
         """Initializes an EvoPy instance.
 
         :param fitness_function: the fitness function on which the individuals are evaluated
@@ -19,7 +20,8 @@ class EvoPy:
         :param num_children: the number of children generated per parent individual
         :param mean: the mean for sampling the random offsets of the initial population
         :param std: the standard deviation for sampling the random offsets of the initial population
-        :param maximize: whether the fitness function should be maximized or minimized.
+        :param maximize: whether the fitness function should be maximized or minimized
+        :param reporter: callback to be invoked at each generation with a ProgressReport as argument
         """
         self.fitness_function = fitness_function
         self.individual_length = individual_length
@@ -30,9 +32,13 @@ class EvoPy:
         self.mean = mean
         self.std = std
         self.maximize = maximize
+        self.reporter = reporter
 
     def run(self):
-        """Run the evolutionary strategy algorithm."""
+        """Run the evolutionary strategy algorithm.
+
+        :return the best genotype found
+        """
         if self.individual_length == 0:
             return None
 
@@ -40,7 +46,7 @@ class EvoPy:
         best = sorted(population, reverse=self.maximize,
                       key=lambda individual: individual.evaluate(self.fitness_function))[0]
 
-        for _ in range(self.generations):
+        for generation in range(self.generations):
             children = [parent.reproduce() for _ in range(self.num_children)
                         for parent in population]
             population = sorted(children + population, reverse=self.maximize,
@@ -51,6 +57,9 @@ class EvoPy:
             else:
                 best = population[0] if population[0].fitness < best.fitness else best
 
+            if self.reporter is not None:
+                self.reporter(ProgressReport(generation, best.genotype, best.fitness))
+
         return best.genotype
 
     def _init_population(self):
diff --git a/evopy/progress_report.py b/evopy/progress_report.py
new file mode 100644
index 0000000..5c1e9d0
--- /dev/null
+++ b/evopy/progress_report.py
@@ -0,0 +1,16 @@
+"""Module containing the ProgressReport class, used to report on the progress of the optimizer."""
+
+
+class ProgressReport:
+    """Class representing a report on an intermediate state of the learning process."""
+
+    def __init__(self, generation, best_genotype, best_fitness):
+        """Initializes the report instance.
+
+        :param generation: number identifying the reported generation
+        :param best_genotype: the genotype of the best individual of that generation
+        :param best_fitness: the fitness of the best individual of that generation
+        """
+        self.generation = generation
+        self.best_genotype = best_genotype
+        self.best_fitness = best_fitness


Test patch:
diff --git a/test/test_evopy.py b/test/test_evopy.py
index 920ecee..e1fc43e 100644
--- a/test/test_evopy.py
+++ b/test/test_evopy.py
@@ -2,7 +2,21 @@
 from evopy import EvoPy
 
 
-def simple_test():
+def test_simple_use_case():
     """Test whether evopy can successfully run for a simple evaluation function."""
     evopy = EvoPy(lambda x: pow(x, 2), 1)
     evopy.run()
+
+
+def test_progress_reporting():
+    """Test whether all generations are reported."""
+    count = [0]
+
+    def reporter(progress_report):
+        assert progress_report.generation == count[0]
+        count[0] += 1
+
+    evopy = EvoPy(lambda x: pow(x, 2), 1, reporter=reporter)
+    evopy.run()
+
+    assert count[0] == 100