Repository: VictorPelaez/coral-reef-optimization-algorithm

Issue:
Refactor Reef initialization
To improve the code architecture, a module with different initialization functions should be considered: one function per mode that accepts all the parameters it needs (`M`, `N`, `L`, `param_grid`, ...) and have it return `REEFpob`. This way we could add documentation for each initialization explaining how it is done and which parameters it needs. Also each function could be easily testable.

Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 8bc2fee..15ab2f9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,6 @@ install:
 - pip install -r requirements.txt
 script:
 - pytest tests/
-
 deploy:
   provider: pypi
   user: victorpelaez
diff --git a/cro/cro.py b/cro/cro.py
index b7231dd..7c24105 100644
--- a/cro/cro.py
+++ b/cro/cro.py
@@ -1,15 +1,24 @@
 #!/usr/bin/env python
 # coding=utf-8
 ###############################################################################
-
-from __future__ import division
+from __future__ import division, print_function
+import sys
+import logging
 import numpy as np
+
+from .reef_initialization import get_reefinit_function
 from .larvaemutation import get_larvaemutation_function
 
 class CRO(object):
     def __init__(self, Ngen, N, M, Fb, Fa, Fd, r0, k, Pd, fitness_coral, opt, L=None,
                  ke=0.2, npolyps=1, seed=None, mode='bin', param_grid={}, verbose=False):
         
+        # Set logging configuration
+        logging_level = logging.INFO if verbose else logging.WARNING
+        logging.basicConfig(stream=sys.stdout,
+                            format="%(message)s")
+        logging.getLogger().setLevel(logging_level)
+
         self.Ngen = Ngen
         self.N    = N
         self.M    = M
@@ -29,9 +38,11 @@ class CRO(object):
         self.mode = mode
         self.param_grid = param_grid
         self.verbose = verbose
-        
-        print("[*Running] Initialization: ", self.opt) 
 
+        self.reefinit_function = get_reefinit_function(mode)
+        self.larvaemutation_function = get_larvaemutation_function(mode)
+        logging.info("Running Initialization: %s", self.opt) 
+        
     def reefinitialization (self):   
         """    
         function [REEF,REEFpob]=reefinitialization(M,N,r0,L)
@@ -45,37 +56,9 @@ class CRO(object):
             - REEF: reef matrix
             - REEFpob: population matrix
         """  
-        
-        # print error. Maybe use other place for all arg-checks
-        if ( (self.param_grid=={}) & (self.mode =='disc') ):
-            print('\nThis mode (', self.mode, ') needs a param_grid as a dictionary')
-            return -1
- 
-        # commom for all modes
-        np.random.seed(seed = self.seed)
-        O = int(np.round(self.N*self.M*self.r0)) # number of occupied reefs 
-        
-        # Binary mode
-        if self.mode =='bin': 
-            A = np.random.randint(2, size=[O, self.L])
-            B = np.zeros([( (self.N*self.M)-O), self.L], int)          
-            REEFpob = np.concatenate([A,B]) # Population creation
-            REEF = np.array((REEFpob.any(axis=1)),int) 
-            return (REEF, REEFpob)
-        
-        # Discrete mode
-        elif self.mode =='disc':
-            for key, value in self.param_grid.items():
-                valmax = (value[1] - value[0] + 1)
-                A = np.random.randint(valmax, size=[O, self.L]) + value[0]
-                B = np.zeros([( (self.N*self.M)-O), self.L], int)
-                REEFpob = np.concatenate([A,B]) # Population creation
-                REEF = np.array((REEFpob.any(axis=1)),int) 
-                return (REEF, REEFpob)
-        
-        else: 
-            print('\nThis mode (', self.mode, ') is not available')
-            return -1
+        np.random.seed(seed = self.seed) # commom for all modes
+        REEF, REEFpob = self.reefinit_function(self.M, self.N, self.r0, self.L, param_grid=self.param_grid)
+        return REEF, REEFpob
 
     def fitness(self, REEFpob):
         """
@@ -163,8 +146,8 @@ class CRO(object):
                 
         pos = np.random.randint(brooders.shape[1], size=(npolyps, nbrooders))
         
-        larvaemutation_function = get_larvaemutation_function(self.mode)
-        brooders = larvaemutation_function(brooders, pos, delta=1, param_grid=self.param_grid, seed=self.seed)
+        brooders = self.larvaemutation_function(brooders, pos, delta=1,
+                                                param_grid=self.param_grid, seed=self.seed)
                                      
         return brooders
    
@@ -384,8 +367,7 @@ class CRO(object):
 
         Bestfitness.append(self.opt_multiplier*np.min(REEFfitness))
         Meanfitness.append(self.opt_multiplier*np.mean(REEFfitness))
-        if verbose:
-            print('Reef initialization:', self.opt_multiplier*np.min(REEFfitness))
+        logging.info('Reef initialization: %s', self.opt_multiplier*np.min(REEFfitness))
 
 
         for n in range(Ngen):
@@ -413,14 +395,13 @@ class CRO(object):
             Meanfitness.append(self.opt_multiplier*np.mean(REEFfitness))
 
             if all([n%10 == 0, n != Ngen, verbose]):
-                print('Best-fitness:', self.opt_multiplier*np.min(REEFfitness), '\n', str(n/Ngen*100) + '% completado \n' );
+                logging.info('Best-fitness: %s, (%.2f%% completado)', self.opt_multiplier*np.min(REEFfitness), n/Ngen*100)
 
-        if verbose:
-            print('Best-fitness:', self.opt_multiplier*np.min(REEFfitness), '\n', str(100) + '% completado \n' ) 
+        logging.info('Best-fitness: %s. (100%% completado)', self.opt_multiplier*np.min(REEFfitness))
         ind_best = np.where(REEFfitness == np.min(REEFfitness))[0][0]
 
         self.plot_results(Bestfitness, Meanfitness)
         print('Best coral: ', REEFpob[ind_best, :])
-        print('Best solution:', self.opt_multiplier*REEFfitness[ind_best])
+        print('Best solution: ', self.opt_multiplier*REEFfitness[ind_best])
         
         return (REEF, REEFpob, REEFfitness, ind_best, Bestfitness, Meanfitness)
diff --git a/cro/larvaemutation.py b/cro/larvaemutation.py
index 835e7f6..20211a7 100644
--- a/cro/larvaemutation.py
+++ b/cro/larvaemutation.py
@@ -87,6 +87,6 @@ def get_larvaemutation_function(mode):
         logging.info("Using {}".format(name))
     else:
         name, func = mode_functions[0]
-        logging.info("Using {} for initializing the reef".format(name))
+        logging.info("Using {} for the brooding operator".format(name))
 
-    return func
\ No newline at end of file
+    return func
diff --git a/cro/reef_initialization.py b/cro/reef_initialization.py
new file mode 100644
index 0000000..211b87f
--- /dev/null
+++ b/cro/reef_initialization.py
@@ -0,0 +1,86 @@
+"""
+Module that contains all the functions that perform the reef
+initialization.
+
+Every function should start with the mode name it performs,
+followed by an underscore and the function name.
+
+It should accept the following arguments:
+    - M: Reef size
+    - N: Reef size
+    - r0: occupied/total ratio
+    - L: coral length
+    - kwargs: extra arguments that the function might need (see
+              https://stackoverflow.com/a/1769475 for an 
+              explanation on kwargs)
+
+It should return a tuple with (REEF, REEFpob)
+"""
+import logging
+
+import numpy as np
+
+from .utils import get_module_functions
+
+def bin_binary(M, N, r0, L, **kwargs):
+    """
+    Each value in each coral in the reef is a boolean value, i.e,
+    either a 0 or a 1
+    """
+    O = int(np.round(N*M*r0)) # number of occupied reefs 
+    A = np.random.randint(2, size=[O, L])
+    B = np.zeros([((N*M)-O), L], int)          
+    REEFpob = np.concatenate([A, B]) # Population creation
+    REEF = np.array((REEFpob.any(axis=1)),int) 
+    return (REEF, REEFpob)
+
+def disc_equal_range(M, N, r0, L, **kwargs):
+    """
+    Each value in each coral in the reef is an integer in the range
+    specified by the keyword argument `param_grid`. `param_grid`
+    must have the next format:
+
+    >>> param_grid = {
+            "x": [2, 10]   
+        }
+
+    where "x" can be basically anything, and its value is a list
+    with both minimum and maximum value.
+    In this example each coral will contain integers between 2 and 10
+    """
+    try:
+        param_grid = kwargs["param_grid"]
+    except KeyError:
+        raise ValueError("disc mode needs a param_grid as a dictionary")
+
+    O = int(np.round(N*M*r0)) # number of occupied reefs 
+    for _, value in param_grid.items():
+        valmax = (value[1] - value[0] + 1)
+        A = np.random.randint(valmax, size=[O, L]) + value[0]
+        B = np.zeros([((N*M)-O), L], int)
+        REEFpob = np.concatenate([A,B]) # Population creation
+        REEF = np.array((REEFpob.any(axis=1)),int)  
+        return (REEF, REEFpob)
+
+"""""
+UTILS
+"""""
+def get_reefinit_function(mode):
+    """
+    Returns the init function for the given mode.
+    If more than one function exists, return one randomly.
+    """
+    reef_init_functions = get_module_functions(__name__)
+    mode_functions = [(name, func) for name, func in reef_init_functions.items()
+                                   if name.startswith(mode)]
+    if not mode_functions:
+        raise ValueError("No initialization function for mode {}".format(mode))
+    elif len(mode_functions) > 1:
+        logging.warning("More than one initialization function for mode {}".format(mode))
+        name, func = mode_functions[0]
+        logging.info("Using {}".format(name))
+    else:
+        name, func = mode_functions[0]
+        logging.info("Using {} for initializing the reef".format(name))
+
+    return func
diff --git a/cro/utils.py b/cro/utils.py
index 5ffd7ff..980e657 100644
--- a/cro/utils.py
+++ b/cro/utils.py
@@ -31,5 +31,3 @@ def get_module_functions(module_name):
     """
     current_module = sys.modules[module_name]
     return dict(getmembers(current_module, predicate=isfunction))
-
-
diff --git a/examples/example_advanced.py b/examples/example_advanced.py
index 9b5d55b..39e1378 100644
--- a/examples/example_advanced.py
+++ b/examples/example_advanced.py
@@ -90,9 +90,9 @@ if __name__ == '__main__':
                             get_prediction=lambda gbr, X: gbr.predict(X), 
                             metric=mean_squared_error)
     start = time.time()
-    cro = CRO(Ngen, N, M, Fb, Fa, Fd, r0, k, Pd, fitness_coral, opt, L, verbose=True)
+    cro = CRO(Ngen, N, M, Fb, Fa, Fd, r0, k, Pd, fitness_coral, opt, L, seed=13, verbose=True)
     (REEF, REEFpob, REEFfitness, ind_best, Bestfitness, Meanfitness) = cro.fit(X, y, gbr)
     print("Example II: feature selection, regression (min mse): ", time.time() - start, "seconds.")
     
     names = np.array(['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'])
-    print(names[REEFpob[ind_best, :]>0])
\ No newline at end of file
+    print(names[REEFpob[ind_best, :]>0])


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..1581836 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,4 @@
+import sys
+import os
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
diff --git a/tests/test_cro.py b/tests/test_cro.py
index f71a78f..5c8a9ed 100644
--- a/tests/test_cro.py
+++ b/tests/test_cro.py
@@ -3,11 +3,6 @@
 ###############################################################################
 # run with:
 # python -m pytest tests/test_cro.py
-
-import sys
-import os
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
 import numpy as np
 
 from cro.cro import CRO
@@ -119,7 +114,6 @@ def test_larvaesettling_nonemptyreef():
     """
     Due to the passed seed,
     [1,0,0,0] will be placed in the empty coral (index 0)
-
     Then, larva [0,1,1,0] will try to settle in indices [0,3,1], settling in the third try (index 1)
     Larva [0,1,0,0] will try in indices [0,3,3], being discarded
     Larva [1,0,0,1] will try in indices [3,3,1], settling in the third try (index 1)
@@ -176,4 +170,4 @@ def test_settle_larvae():
     
     assert REEF_res[indices] == 1
     np.testing.assert_almost_equal(REEFpob_res[indices, :], larvae)
-    np.testing.assert_almost_equal(REEFfitness_res[indices], larvaefitness)
\ No newline at end of file
+    np.testing.assert_almost_equal(REEFfitness_res[indices], larvaefitness)
diff --git a/tests/test_fitness.py b/tests/test_fitness.py
index 30f0a0e..73eafe7 100644
--- a/tests/test_fitness.py
+++ b/tests/test_fitness.py
@@ -3,11 +3,6 @@
 ###############################################################################
 # run with:
 # python -m pytest tests/test_fitness.py
-
-import sys
-import os
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
 import numpy as np
 
 from cro.fitness import max_ones
@@ -37,4 +32,4 @@ def test_feature_selection():
     """
     To be added
     """
-    pass
\ No newline at end of file
+    pass
diff --git a/tests/test_larvaemutation.py b/tests/test_larvaemutation.py
index 9713491..c564422 100644
--- a/tests/test_larvaemutation.py
+++ b/tests/test_larvaemutation.py
@@ -3,11 +3,6 @@
 ###############################################################################
 # run with:
 # python -m pytest tests/test_larvaemutation.py
-
-import sys
-import os
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
 import numpy as np
 
 from cro.larvaemutation import get_larvaemutation_function
@@ -79,4 +74,4 @@ def test_get_larvaemutation_function():
     f = get_larvaemutation_function('bin')
     assert 'function bin_larvaemutation' in str(f)
     f = get_larvaemutation_function('disc')
-    assert 'function disc_larvaemutation' in str(f)
\ No newline at end of file
+    assert 'function disc_larvaemutation' in str(f)
diff --git a/tests/test_reefinitialization.py b/tests/test_reefinitialization.py
index 567c7dd..198f6bd 100644
--- a/tests/test_reefinitialization.py
+++ b/tests/test_reefinitialization.py
@@ -3,28 +3,28 @@
 ###############################################################################
 # run with:
 # python -m pytest tests/test_larvaemutation.py
-
-import sys
-import os
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-
 import numpy as np
 
-#from cro.reefinitialization import bin_binary, disc_equal_range
+from cro.reef_initialization import bin_binary, disc_equal_range
 
 # ------------------------------------------------------
 # reefinitialization module
 # ------------------------------------------------------
-
 def test_bin_binary():
     """
     Test that corals in the population only contain values in {0, 1}
     """
-    pass
-    
-    
+    M, N, r0, L = 2, 2, 0.6, 8
+    REEF, REEFpob = bin_binary(M, N, r0, L)
+    assert set(REEFpob.ravel()) == {0, 1}
+
 def test_disc_equal_range():
     """
     Test that corals in population contain values specified in the grid
     """
-    pass
\ No newline at end of file
+    M, N, r0, L = 2, 2, 0.6, 8
+    grid = {'x': [2, 10]}      # Discrete values between 2 and 10
+
+    REEF, REEFpob = disc_equal_range(M, N, r0, L, param_grid=grid)
+    p = sum(REEFpob[np.where(REEFpob!=0)]<grid['x'][0]) + sum(REEFpob[np.where(REEFpob!=0)]>grid['x'][1])
+    assert p == 0