Repository: AureumChaos/LEAP

Issue:
Boolean representation outputs boolean values instead of binary strings.
LEAP binary representation uses a numpy binary vector.  (See, binary_rep.initializers.random_bernoulli_vector()). The string representation returns a vector of booleans instead of the expected binary string.  E.g.,

n = np.random.rand(*shape) < 0.5
n
Out[6]: array([False, False, False, False])

That should be [0,0,0,0].

You can cast that to a vector of integers:

n.astype(int)
Out[7]: array([0, 0, 0, 0])

But then you presumably lose the compressed and optimized binary representation used by numpy.

So, ideally we'd like to keep that internal optimized binary representation while also printing ones and zeroes.

Discussion:

Patch:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index e875bbf..5549377 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -22,6 +22,7 @@ python:
   install:
     - requirements: docs/requirements.txt
 
+
 build:
   os: ubuntu-22.04
   tools:
diff --git a/examples/simple/simple_ga.py b/examples/simple/simple_ga.py
index 85bca06..3dd7087 100644
--- a/examples/simple/simple_ga.py
+++ b/examples/simple/simple_ga.py
@@ -30,7 +30,7 @@ if __name__ == '__main__':
     parents = Individual.evaluate_population(parents)
 
     # print initial, random population
-    util.print_population(parents, generation=0)
+    probe.print_population(parents, generation=0)
 
 
     # When running the test harness, just run for two generations
@@ -65,4 +65,4 @@ if __name__ == '__main__':
 
         # Just to demonstrate that we can also get the current generation from
         # the context
-        util.print_population(parents, context['leap']['generation'])
+        probe.print_population(parents, context['leap']['generation'])
diff --git a/leap_ec/multiobjective/ops.py b/leap_ec/multiobjective/ops.py
index 5b6fc9b..3f9749c 100644
--- a/leap_ec/multiobjective/ops.py
+++ b/leap_ec/multiobjective/ops.py
@@ -175,12 +175,6 @@ def per_rank_crowding_calc(ranked_population: list, is_maximizing) -> list:
     :param ranked_population: A population of entirely one rank
     :returns: population with crowding distance calculate for one rank
     """
-    if len(ranked_population) == 1:
-        # There is just one individual in this rank, so set their distance to 0
-        # since there are no neighbors.
-        ranked_population[0].distance = 0
-        return ranked_population
-
     # Presuming this is a population with homogeneous objectives, then the size of
     # the optimization directions array should be equal to the number of objectives.
     num_objectives = is_maximizing.shape[0]
@@ -218,7 +212,7 @@ def per_rank_crowding_calc(ranked_population: list, is_maximizing) -> list:
     sorted_pop = []
 
     for objective in range(num_objectives):
-        if objective_ranges[objective] == 0 or objective_ranges[objective] == np.nan:
+        if objective_ranges[objective] == 0:
             continue
         
         # sort by objective being mindful that maximization vs. minimization may
@@ -245,8 +239,7 @@ def per_rank_crowding_calc(ranked_population: list, is_maximizing) -> list:
                                             objective]) / objective_ranges[
                                             objective]
 
-    # return ranked_population not sorted since sometimes sorted_pop will be empty
-    return ranked_population
+    return sorted_pop
 
 @wrap_curry
 @listlist_op
diff --git a/leap_ec/probe.py b/leap_ec/probe.py
index bd58cd1..6fdb50f 100644
--- a/leap_ec/probe.py
+++ b/leap_ec/probe.py
@@ -52,23 +52,44 @@ def print_probe(population, probe, stream=sys.stdout, prefix=''):
 @wrap_curry
 @iteriter_op
 def print_individual(next_individual: Iterator, prefix='',
-                     stream=sys.stdout) -> Iterator:
+                     numpy_as_list=False, stream=sys.stdout) -> Iterator:
     """ Just echoes the individual from within the pipeline
 
     Uses next_individual.__str__
 
     :param next_individual: iterator for next individual to be printed
+    :param prefix: prefix appended to the start of the line
+    :param numpy_as_list: If True, numpy arrays are converted to lists before printing
+    :param stream: File object passed to print
     :return: the same individual, unchanged
     """
 
     while True:
         individual = next(next_individual)
 
-        print(f'{prefix}{individual!s}', file=stream)
+        with _maybe_list(numpy_as_list):
+            print(f'{prefix}{individual!s}', file=stream)
 
         yield individual
 
 
+###############################
+# Function print_population
+###############################
+def print_population(population, generation, numpy_as_list=False):
+    """ Convenience function for pretty printing a population that's
+    associated with a given generation
+
+    :param population: The population of individuals to be printed
+    :param generation: The generation of the population
+    :param numpy_as_list: If True, numpy arrays are converted to lists before printing
+    :return: None
+    """
+    with _maybe_list(numpy_as_list):
+        for individual in population:
+            print(generation, individual.genome, individual.fitness)
+
+
 ##############################
 # Class BestSoFarProbe
 ##############################
@@ -216,11 +237,16 @@ def _maybe_list(numpy_as_list):
     This uses a context manager so if anything preemptively terminates during
     writing, say by stopping a jupyter cell, default behavior is restored.
 
+    Now also modifies booleans to print as binary integers instead.
+    FIXME Make this into a fully featured pretty printting context manager
+
     :param numpy_as_list: whether or not in the scope of this context
         manager numpy arrays should be formatted as python lists.
     """
 
     def to_str_list(arr):
+        if arr.dtype == np.bool_:
+            return str([int(b) for b in arr])
         return str(arr.tolist())
 
     if numpy_as_list:
@@ -232,7 +258,8 @@ def _maybe_list(numpy_as_list):
             np.set_string_function(None, True)
             np.set_string_function(None, False)
     else:
-        yield
+        with np.printoptions(formatter={"bool": lambda x: str(int(x))}):
+            yield
 
 
 ##############################
diff --git a/leap_ec/util.py b/leap_ec/util.py
index e45e889..440d881 100644
--- a/leap_ec/util.py
+++ b/leap_ec/util.py
@@ -26,21 +26,6 @@ def wrap_curry(f):
     return wraps(f)(curry(f))
 
 
-###############################
-# Function print_population
-###############################
-def print_population(population, generation):
-    """ Convenience function for pretty printing a population that's
-    associated with a given generation
-
-    :param population:
-    :param generation:
-    :return: None
-    """
-    for individual in population:
-        print(generation, individual.genome, individual.fitness)
-
-
 ###############################
 # Function is_sequence
 ###############################


Test patch:
diff --git a/tests/distributed/test_evaluate.py b/tests/distributed/test_evaluate.py
index 5f25845..cbc71b4 100644
--- a/tests/distributed/test_evaluate.py
+++ b/tests/distributed/test_evaluate.py
@@ -3,7 +3,7 @@
 """
 import numpy as np
 import pytest
-from distributed import Client, LocalCluster
+from distributed import Client
 
 from leap_ec.binary_rep.problems import MaxOnes
 from leap_ec.distrib.evaluate import evaluate, is_viable
@@ -18,7 +18,7 @@ def test_good_eval():
         leap_ec.distrib.evaluate works for normal circumstances.
     """
     # set up a basic dask local cluster
-    with Client(LocalCluster(n_workers=1, threads_per_worker=1)) as client:
+    with Client() as client:
         # hand craft an individual that should evaluate fine
         # Let's try evaluating a single individual
         individual = Individual(np.array([1, 1]),
@@ -40,7 +40,7 @@ def test_broken_individual_eval():
         TODO implement this
     """
     # set up a basic dask local cluster
-    with Client(LocalCluster(n_workers=1, threads_per_worker=1)) as client:
+    with Client() as client:
         # hand craft an individual that will intentionally fail by not
         # assigning it a Problem class
         individual = DistributedIndividual(np.array([1, 1]),
diff --git a/tests/readmetests/pipeline.py b/tests/readmetests/pipeline.py
index 7645462..9d50342 100644
--- a/tests/readmetests/pipeline.py
+++ b/tests/readmetests/pipeline.py
@@ -12,7 +12,7 @@ import leap_ec.ops as ops
 from leap_ec.binary_rep.problems import MaxOnes
 from leap_ec.binary_rep.initializers import create_binary_sequence
 from leap_ec.binary_rep.ops import mutate_bitflip
-from leap_ec import util
+from leap_ec import util, probe
 
 # create initial rand population of 5 individuals
 parents = Individual.create_population(5,
@@ -23,7 +23,7 @@ parents = Individual.create_population(5,
 parents = Individual.evaluate_population(parents)
 
 # print initial, random population
-util.print_population(parents, generation=0)
+probe.print_population(parents, generation=0)
 
 # generation_counter is an optional convenience for generation tracking
 generation_counter = util.inc_generation(context=context)
@@ -41,4 +41,4 @@ while generation_counter.generation() < 6:
 
     generation_counter()  # increment to the next generation
 
-    util.print_population(parents, context['leap']['generation'])
+    probe.print_population(parents, context['leap']['generation'])