Repository: AureumChaos/LEAP

Issue:
`genome_mutate_binomial()` needs to change from returning a function to accepted a genome and an `expected_num_mutations` argument.
There is an unwritten standard (which needs to be written) for mutation "helper" functions that they accept an `expected_num_mutations` argument.  Another expectation is that calling that function should mutate a genome passed as a positional argument.  Instead the current version returns a function that accepts a genome argument and does not accept an `expected_num_mutations` argument, which thwarts use of `segmented_rep.ops.apply_mutation()`.  (See also, #209 )

Discussion:

Patch:
diff --git a/leap_ec/executable_rep/rules.py b/leap_ec/executable_rep/rules.py
index 87aa9da..1157078 100644
--- a/leap_ec/executable_rep/rules.py
+++ b/leap_ec/executable_rep/rules.py
@@ -244,8 +244,7 @@ class PittRulesDecoder(Decoder):
 
                 return segment
 
-            rule_set = create_segmented_sequence(num_rules, create_rule)
-            return rule_set()
+            return create_segmented_sequence(num_rules, create_rule)
 
         return create_rule_set
 
diff --git a/leap_ec/int_rep/ops.py b/leap_ec/int_rep/ops.py
index 1967e22..7e990ba 100644
--- a/leap_ec/int_rep/ops.py
+++ b/leap_ec/int_rep/ops.py
@@ -120,7 +120,7 @@ def mutate_binomial(next_individual: Iterator, std: float, bounds: list,
                     expected_num_mutations: float = None,
                     probability: float = None,
                     n: int = 10000) -> Iterator:
-    """Mutate genes by adding an integer offset sampled from a binomial distribution
+    """ Mutate genes by adding an integer offset sampled from a binomial distribution
     centered on the current gene value.
 
     This is very similar to applying additive Gaussian mutation and then rounding to
@@ -155,14 +155,14 @@ def mutate_binomial(next_individual: Iterator, std: float, bounds: list,
     >>> mutated = next(operator(population))
 
     .. note::
-        The binomial distribution is defined by two parameters, `n` and `p`.  Here we 
+        The binomial distribution is defined by two parameters, `n` and `p`.  Here we
         simplify the interface by asking instead for an `std` parameter, and fixing
         a high value of `n` by default.  The value of `p` needed to obtain the
         given `std` is computed for you internally.
 
         As the plots below illustrate, the binomial distribution is approximated by a
         Gaussian.  For high `n` and large standard deviations, the two are effectively
-        equivalent.  But when the standard deviation (and thus binomial `p` parameter) 
+        equivalent.  But when the standard deviation (and thus binomial `p` parameter)
         is relatively small, the approximation becomes less accurate, and the binomial
         differs somewhat from a Gaussian.
 
@@ -231,7 +231,6 @@ def mutate_binomial(next_individual: Iterator, std: float, bounds: list,
 ##############################
 # Function genome_mutate_binomial
 ##############################
-@curry
 def genome_mutate_binomial(std,
                         bounds: list,
                         expected_num_mutations: float = None,
@@ -252,14 +251,19 @@ def genome_mutate_binomial(std,
     assert((probability is None) or (probability >= 0))
     assert((probability is None) or (probability <= 1))
 
-
+    # Is the only reason we're making this a closure is to save from having to
+    # do this calculation with each mutation? -- Mark
     if isinstance(std, Iterable):
         p = np.array([_binomial_p_from_std(n, s) for s in std])
     else:
         p = _binomial_p_from_std(n, std)
 
-    def mutator(genome):
+    def mutator(genome,
+                expected_num_mutations: float = expected_num_mutations,
+                probability: float = probability):
         """Function to return as a closure."""
+        # Make this check here, too, since this is called within the pipeline
+        # and may be invoked dynamically with different parameters.
         if not isinstance(genome, np.ndarray):
             raise ValueError(("Expected genome to be a numpy array. "
                             f"Got {type(genome)}."))
@@ -278,11 +282,11 @@ def genome_mutate_binomial(std,
         selected_p_values = p if not isinstance(p, Iterable) else p[indices_to_mutate]
         binom_mean = n*selected_p_values  # this will do elementwise multiplication if p is a vector
 
-        # Apply binomial pertebations
+        # Apply binomial perturbations
         additive = np.random.binomial(n, selected_p_values, size=len(indices_to_mutate)) - np.floor(binom_mean)
         mutated = genome[indices_to_mutate] + additive
         genome[indices_to_mutate] = mutated
-        
+
         genome = apply_hard_bounds(genome, bounds).astype(datatype)
 
         # consistency check on data type
@@ -295,8 +299,11 @@ def genome_mutate_binomial(std,
 def _binomial_p_from_std(n, std):
     """Given a number of 'coin flips' n, compute the value of p that is
     needed to achieve a desired standard deviation."""
-    if (4*std**2/n > 1):
-        raise ValueError(f"The provided value of n ({n}) is too low to support a Binomial distribution with a standard deviation of {std}.  Choose a higher value of n, or reduce the std.")
+    if 4 * std ** 2 / n > 1:
+        raise ValueError(f"The provided value of n ({n}) is too low to "
+                         f"support a Binomial distribution with a stand"
+                         f"ard deviation of {std}.  Choose a higher value of "
+                         f"n, or reduce the std.")
     # We arrived at this expression by noting that σ^2 = np(1-p)
     # and solving for p via the quadratic formula
-    return (1 - np.sqrt(1-4*std**2/n))/2
+    return (1 - np.sqrt(1 - 4 * std ** 2 / n)) / 2
diff --git a/leap_ec/probe.py b/leap_ec/probe.py
index 169c422..5711ee8 100644
--- a/leap_ec/probe.py
+++ b/leap_ec/probe.py
@@ -315,8 +315,8 @@ class AttributesCSVProbe(op.Operator):
     You can retrieve the result programatically from the `dataframe` property:
 
     >>> probe.dataframe
-       step  fitness           genome
-    0   100        4  [0, 1, 1, 1, 1]
+       step  fitness       genome
+    0   100        4  [0 1 1 1 1]
 
     By default, the results are also written to `sys.stdout`.  You can pass
     any file object you like into the `stream` parameter.
@@ -440,7 +440,7 @@ class AttributesCSVProbe(op.Operator):
         if self.do_fitness:
             row['fitness'] = ind.fitness
         if self.do_genome:
-            row['genome'] = ind.genome
+            row['genome'] = str(ind.genome)
         for k, f in self.extra_metrics.items():
             row[k] = f(row)
 
diff --git a/leap_ec/segmented_rep/initializers.py b/leap_ec/segmented_rep/initializers.py
index bb00d5f..857a3bf 100644
--- a/leap_ec/segmented_rep/initializers.py
+++ b/leap_ec/segmented_rep/initializers.py
@@ -19,8 +19,7 @@ def create_segmented_sequence(length, seq_initializer):
     to calculate the number of segments to generate.
 
     >>> from leap_ec.binary_rep.initializers import create_binary_sequence
-    >>> segmented_initializer = create_segmented_sequence(3, create_binary_sequence(3))
-    >>> segments = segmented_initializer()
+    >>> segments = create_segmented_sequence(3, create_binary_sequence(3))
     >>> assert len(segments) == 3
 
 
@@ -28,17 +27,20 @@ def create_segmented_sequence(length, seq_initializer):
     :type length: int or Callable
     :param seq_initializer: initializer for creating individual sequences
     :type seq_initializer: Callable
-    :return: function that returns a list of segmented
-    :rtype: Callable
+    :return: test_sequence of segments
+    :rtype: list
     """
     if callable(length):
         num_segments = length()
     else:
         num_segments = length
 
-    def segmented():
-        segments = [seq_initializer() for _ in range(num_segments)]
-        return segments
+    if not hasattr(seq_initializer, '__len__'):
+        seq_initializer = [ seq_initializer for _ in range(num_segments) ]
 
-    return segmented
+    assert(len(seq_initializer) == num_segments)
+
+    segments = [ init() for init in seq_initializer ]
+
+    return segments
 
diff --git a/leap_ec/segmented_rep/ops.py b/leap_ec/segmented_rep/ops.py
index 23565e1..9bfc24c 100644
--- a/leap_ec/segmented_rep/ops.py
+++ b/leap_ec/segmented_rep/ops.py
@@ -75,11 +75,11 @@ def segmented_mutate(next_individual: Iterator, mutator_functions: list):
     while True:
         individual = next(next_individual)
         assert(len(individual.genome) == len(mutator_functions)), f"Found {len(individual.genome)} segments in this genome, but we've got {len(mutators)} mutators."
-        
+
         mutated_genome = []
         for segment, m in zip(individual.genome, mutator_functions):
             mutated_genome.append(m(segment))
-        
+
         individual.genome = mutated_genome
 
         # invalidate the fitness since we have a modified genome


Test patch:
diff --git a/tests/segmented_rep/test_initializers.py b/tests/segmented_rep/test_initializers.py
index 5293570..a90f749 100644
--- a/tests/segmented_rep/test_initializers.py
+++ b/tests/segmented_rep/test_initializers.py
@@ -19,16 +19,13 @@ def gen_sequence():
 def test_segmented_initializer_fixed_length():
     """ created fixed length segments """
 
-    segments_init = create_segmented_sequence(1, gen_sequence)
-    segments = segments_init()
+    segments = create_segmented_sequence(1, gen_sequence)
     assert segments == [test_sequence]
 
-    segments_init = create_segmented_sequence(2, gen_sequence)
-    segments = segments_init()
+    segments = create_segmented_sequence(2, gen_sequence)
     assert segments == [test_sequence, test_sequence]
 
-    segments_init = create_segmented_sequence(3, gen_sequence)
-    segments = segments_init()
+    segments = create_segmented_sequence(3, gen_sequence)
     assert segments == [test_sequence, test_sequence, test_sequence]
 
 
@@ -50,9 +47,8 @@ def test_segmented_initializer_variable_length():
     for i in range(N):
         # randomly generate a sequence of segments with the number of segments
         # drawn from a uniform distribution
-        initializer = create_segmented_sequence(distribution_func,
-                                                gen_sequence)
-        segments.append(initializer())
+        segments.append(create_segmented_sequence(distribution_func,
+                                                  gen_sequence))
 
         # track the lengths of those segments
         segment_lengths.append(len(segments[-1]))
diff --git a/tests/segmented_rep/test_ops.py b/tests/segmented_rep/test_ops.py
index 1d366bc..860f198 100644
--- a/tests/segmented_rep/test_ops.py
+++ b/tests/segmented_rep/test_ops.py
@@ -1,7 +1,9 @@
 """ Unit tests for pipeline operators in the segmented representation package. """
+import functools
 import numpy as np
 import pytest
 from leap_ec.binary_rep.ops import genome_mutate_bitflip
+from leap_ec.int_rep.ops import genome_mutate_binomial, individual_mutate_randint
 from leap_ec.individual import Individual
 from leap_ec.ops import n_ary_crossover
 from leap_ec.segmented_rep.ops import apply_mutation, remove_segment, \
@@ -10,13 +12,17 @@ from leap_ec.segmented_rep.ops import apply_mutation, remove_segment, \
 ##############################
 # Test Fixtures
 ##############################
-test_sequence = np.array([2,2]) # just an arbitrary sequence of twos for testing
+test_sequence = np.array(
+    [2, 2])  # just an arbitrary sequence of twos for testing
+
 
 @pytest.fixture
 def gen_sequence():
     """Return a function that returns an arbitrary static test_sequence."""
+
     def f():
         return test_sequence
+
     return f
 
 
@@ -31,18 +37,51 @@ def in_possible_outcomes(test_seq, possible_outcomes):
 def test_apply_mutation():
     """Applying segment-wise mutation operators with expected_num_mutations=len(genome) should
     result in every gene of every segment being mutated."""
-    mutation_op = apply_mutation(mutator=genome_mutate_bitflip, expected_num_mutations=4)
-    original = Individual([np.array([0,0]), np.array([1,1])])
+    mutation_op = apply_mutation(mutator=genome_mutate_bitflip,
+                                 expected_num_mutations=4)
+    original = Individual([np.array([0, 0]), np.array([1, 1])])
     mutated = next(mutation_op(iter([original])))
 
     assert np.all(mutated.genome[0] == [1, 1]) \
-        and np.all(mutated.genome[1] == [0, 0])
+           and np.all(mutated.genome[1] == [0, 0])
+
+
+def test_apply_mutation_uniform_int():
+    """ Same test, but with integer mutation """
+    mutator = individual_mutate_randint(bounds=[(0, 10), (100, 110)])
+
+    mutation_op = apply_mutation(mutator=mutator,
+                                 expected_num_mutations=2)
+    original = Individual([np.array([0, 100]), np.array([1, 101])])
+    mutated = next(mutation_op(iter([original])))
+
+    # TODO add checks (didn't add at this time because we wanted to ensure
+    # that this would even generate valid behavior w/o an exception.
+
+    pass
+
+
+def test_apply_mutation_binomial_int():
+    """ Same test, but with integer mutation """
+    mutator = genome_mutate_binomial(
+        std=1.0,
+        bounds=[(0, 10), (100, 110)],
+        probability=1)
+    mutation_op = functools.partial(apply_mutation,
+                                    mutator=mutator)
+    original = Individual([np.array([0, 100]), np.array([1, 101])])
+    mutated = next(mutation_op(iter([original])))
+
+    pass
+
 
 ##############################
 # Tests for remove_segment()
 ##############################
+
+
 def test_segmented_remove():
-    original = Individual([np.array([0,0]), np.array([1,1])])
+    original = Individual([np.array([0, 0]), np.array([1, 1])])
     mutated = next(remove_segment(iter([original]), probability=1.0))
     assert (mutated.genome[0] == [np.array([0, 0]), np.array([1, 1])]).any()
 
@@ -52,7 +91,7 @@ def test_segmented_remove():
 ##############################
 def test_segmented_add(gen_sequence):
     # Test with append first
-    original = Individual([np.array([0,0]), np.array([1,1])])
+    original = Individual([np.array([0, 0]), np.array([1, 1])])
     mutated = next(add_segment(iter([original]),
                                seq_initializer=gen_sequence,
                                probability=1.0,
@@ -66,7 +105,7 @@ def test_segmented_add(gen_sequence):
                          [np.array([0, 0]), np.array([1, 1]), test_sequence]]
 
     for i in range(20):
-        original = Individual([np.array([0,0]), np.array([1,1])])
+        original = Individual([np.array([0, 0]), np.array([1, 1])])
         mutated = next(add_segment(iter([original]),
                                    seq_initializer=gen_sequence,
                                    probability=1.0,
@@ -79,7 +118,7 @@ def test_segmented_add(gen_sequence):
 # Tests for copy_segment()
 ##############################
 def test_segmented_copy():
-    original = Individual([np.array([0,0]), np.array([1,1])])
+    original = Individual([np.array([0, 0]), np.array([1, 1])])
     mutated = next(copy_segment(iter([original]),
                                 probability=1.0,
                                 append=True))
@@ -107,7 +146,6 @@ def test_segmented_copy():
         assert in_possible_outcomes(mutated.genome, possible_outcomes)
 
 
-
 ##############################
 # Tests for n_ary_crossover() on segmented genomes
 ##############################
@@ -115,8 +153,8 @@ def test_segmented_crossover():
     """ test that n-ary crossover works as expected for fixed and variable
         length segments
     """
-    a = Individual([np.array([0,0]), np.array([1,1])])
-    b = Individual([np.array([1,1]), np.array([0,0])])
+    a = Individual([np.array([0, 0]), np.array([1, 1])])
+    b = Individual([np.array([1, 1]), np.array([0, 0])])
 
     result = n_ary_crossover(iter([a, b]))
     c = next(result)