From 0d54df9b24e9a62fd9c7738ef8fb48b82f781aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Mon, 9 Feb 2015 13:59:34 -0500 Subject: [PATCH 01/26] Replace type by MetaCreator to support better pickling. --- deap/creator.py | 68 ++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/deap/creator.py b/deap/creator.py index 468d823f1..fef2eb064 100644 --- a/deap/creator.py +++ b/deap/creator.py @@ -24,6 +24,7 @@ import array import copy import warnings +import copy_reg class_replacers = {} """Some classes in Python's standard library as well as third party library @@ -91,6 +92,46 @@ def __reduce__(self): return (self.__class__, (list(self),), self.__dict__) class_replacers[array.array] = _array +class MetaCreator(type): + def __new__(meta, name, base, dct): + return super(MetaCreator, meta).__new__(meta, name, (base,), dct) + + def __init__(cls, name, base, dct): + # A DeprecationWarning is raised when the object inherits from the + # class "object" which leave the option of passing arguments, but + # raise a warning stating that it will eventually stop permitting + # this option. Usually this happens when the base class does not + # override the __init__ method from object. + dict_inst = {} + dict_cls = {} + for obj_name, obj in dct.iteritems(): + if isinstance(obj, type): + dict_inst[obj_name] = obj + else: + dict_cls[obj_name] = obj + def initType(self, *args, **kargs): + """Replace the __init__ function of the new type, in order to + add attributes that were defined with **kargs to the instance. + """ + for obj_name, obj in dict_inst.iteritems(): + setattr(self, obj_name, obj()) + if base.__init__ is not object.__init__: + base.__init__(self, *args, **kargs) + + cls.__init__ = initType + cls.reduce_args = (name, base, dct) + super(MetaCreator, cls).__init__(name, (base,), dict_cls) + + def __reduce__(cls): + return (meta_create, cls.reduce_args) + +copy_reg.pickle(MetaCreator, MetaCreator.__reduce__) + +def meta_create(name, base, dct): + class_ = MetaCreator(name, base, dct) + globals()[name] = class_ + return class_ + def create(name, base, **kargs): """Creates a new class named *name* inheriting from *base* in the :mod:`~deap.creator` module. The new class can have attributes defined by @@ -138,32 +179,7 @@ def __init__(self): "creation of that class or rename it.".format(name), RuntimeWarning) - dict_inst = {} - dict_cls = {} - for obj_name, obj in kargs.iteritems(): - if isinstance(obj, type): - dict_inst[obj_name] = obj - else: - dict_cls[obj_name] = obj - # Check if the base class has to be replaced if base in class_replacers: base = class_replacers[base] - - # A DeprecationWarning is raised when the object inherits from the - # class "object" which leave the option of passing arguments, but - # raise a warning stating that it will eventually stop permitting - # this option. Usually this happens when the base class does not - # override the __init__ method from object. - def initType(self, *args, **kargs): - """Replace the __init__ function of the new type, in order to - add attributes that were defined with **kargs to the instance. - """ - for obj_name, obj in dict_inst.iteritems(): - setattr(self, obj_name, obj()) - if base.__init__ is not object.__init__: - base.__init__(self, *args, **kargs) - - objtype = type(str(name), (base,), dict_cls) - objtype.__init__ = initType - globals()[name] = objtype + meta_create(name, base, kargs) From 1d0c65d9bcd6a5b29a4fe4ee9b0e5167d149dbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Mon, 9 Feb 2015 16:20:46 -0500 Subject: [PATCH 02/26] Update onemax_mp to take advantage of MetaCreator. --- examples/ga/onemax_mp.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/ga/onemax_mp.py b/examples/ga/onemax_mp.py index caed5444d..e09304758 100755 --- a/examples/ga/onemax_mp.py +++ b/examples/ga/onemax_mp.py @@ -30,29 +30,28 @@ from deap import creator from deap import tools +def evalOneMax(individual): + return sum(individual), -creator.create("FitnessMax", base.Fitness, weights=(1.0,)) -creator.create("Individual", array.array, typecode='b', fitness=creator.FitnessMax) - -toolbox = base.Toolbox() +def main(seed): + random.seed(seed) -# Attribute generator -toolbox.register("attr_bool", random.randint, 0, 1) + creator.create("FitnessMax", base.Fitness, weights=(1.0,)) + creator.create("Individual", array.array, typecode='b', fitness=creator.FitnessMax) -# Structure initializers -toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, 100) -toolbox.register("population", tools.initRepeat, list, toolbox.individual) + toolbox = base.Toolbox() -def evalOneMax(individual): - return sum(individual), + # Attribute generator + toolbox.register("attr_bool", random.randint, 0, 1) -toolbox.register("evaluate", evalOneMax) -toolbox.register("mate", tools.cxTwoPoint) -toolbox.register("mutate", tools.mutFlipBit, indpb=0.05) -toolbox.register("select", tools.selTournament, tournsize=3) + # Structure initializers + toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_bool, 100) + toolbox.register("population", tools.initRepeat, list, toolbox.individual) -if __name__ == "__main__": - random.seed(64) + toolbox.register("evaluate", evalOneMax) + toolbox.register("mate", tools.cxTwoPoint) + toolbox.register("mutate", tools.mutFlipBit, indpb=0.05) + toolbox.register("select", tools.selTournament, tournsize=3) # Process Pool of 4 workers pool = multiprocessing.Pool(processes=4) @@ -70,3 +69,6 @@ def evalOneMax(individual): stats=stats, halloffame=hof) pool.close() + +if __name__ == "__main__": + main(64) From f6854c2bd8a5bb82d4ee462ffa7853ff2c32cb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Sun, 22 Feb 2015 14:22:51 -0500 Subject: [PATCH 03/26] Replace Ephemeral by MetaEphemeral. The usage of metaclass allow us to stop registering Ephemeral classes in the gp module global scope. We also register the __reduce__ function of MetaEphemeral so Ephemeral could easily be pickled, as long as the ephemeral function is picklable (i.e.: no lambda function). The metaclass also has a cache based on class id to avoid creating new object for the same ephemeral when unpickling. --- deap/gp.py | 77 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/deap/gp.py b/deap/gp.py index 06abd5e74..b7e01162c 100644 --- a/deap/gp.py +++ b/deap/gp.py @@ -21,6 +21,7 @@ """ import copy import math +import copy_reg import random import re import sys @@ -28,7 +29,6 @@ from collections import defaultdict, deque from functools import partial, wraps -from inspect import isclass from operator import eq, lt import tools # Needed by HARM-GP @@ -232,20 +232,36 @@ def __eq__(self, other): else: return NotImplemented -class Ephemeral(Terminal): - """Class that encapsulates a terminal which value is set when the +class MetaEphemeral(type): + """Meta-Class that creates a terminal which value is set when the object is created. To mutate the value, a new object has to be - generated. This is an abstract base class. When subclassing, a - staticmethod 'func' must be defined. + generated. """ - def __init__(self): - Terminal.__init__(self, self.func(), symbolic=False, ret=self.ret) + cache = {} + def __new__(meta, name, func, ret=__type__, id_=None): + if id_ in MetaEphemeral.cache: + return MetaEphemeral.cache[id_] - @staticmethod - def func(): - """Return a random value used to define the ephemeral state. - """ - raise NotImplementedError + def __init__(self): + self.value = func() + + attr = {'__init__' : __init__, + 'name' : name, + 'func' : func, + 'ret' : ret, + 'conv_fct' : repr} + + cls = super(MetaEphemeral, meta).__new__(meta, name, (Terminal,), attr) + MetaEphemeral.cache[id(cls)] = cls + return cls + + def __init__(cls, name, func, ret=__type__, id_=None): + super(MetaEphemeral, cls).__init__(name, (Terminal,), {}) + + def __reduce__(cls): + return (MetaEphemeral, (cls.name, cls.func, cls.ret, id(cls))) + +copy_reg.pickle(MetaEphemeral, MetaEphemeral.__reduce__) class PrimitiveSetTyped(object): """Class that contains the primitives that can be used to solve a @@ -379,24 +395,17 @@ def addEphemeralConstant(self, name, ephemeral, ret_type): :param ephemeral: function with no arguments returning a random value. :param ret_type: type of the object returned by *ephemeral*. """ - module_gp = globals() - if not name in module_gp: - class_ = type(name, (Ephemeral,), {'func' : staticmethod(ephemeral), - 'ret' : ret_type}) - module_gp[name] = class_ + if not name in self.mapping: + class_ = MetaEphemeral(name, ephemeral, ret_type) else: - class_ = module_gp[name] - if issubclass(class_, Ephemeral): - if class_.func is not ephemeral: - raise Exception("Ephemerals with different functions should " - "be named differently, even between psets.") - elif class_.ret is not ret_type: - raise Exception("Ephemerals with the same name and function " - "should have the same type, even between psets.") - else: - raise Exception("Ephemerals should be named differently " - "than classes defined in the gp module.") - + class_ = self.mapping[name] + if class_.func is not ephemeral: + raise Exception("Ephemerals with different functions should " + "be named differently, even between psets.") + if class_.ret is not ret_type: + raise Exception("Ephemerals with the same name and function " + "should have the same type, even between psets.") + self._add(class_) self.terms_count += 1 @@ -595,7 +604,7 @@ def generate(pset, min_, max_, condition, type_=None): raise IndexError, "The gp.generate function tried to add "\ "a terminal of type '%s', but there is "\ "none available." % (type_,), traceback - if isclass(term): + if type(term) is MetaEphemeral: term = term() expr.append(term) else: @@ -747,7 +756,7 @@ def mutNodeReplacement(individual, pset): if node.arity == 0: # Terminal term = random.choice(pset.terminals[node.ret]) - if isclass(term): + if type(term) is MetaEphemeral: term = term() individual[index] = term else: # Primitive @@ -772,7 +781,7 @@ def mutEphemeral(individual, mode): ephemerals_idx = [index for index, node in enumerate(individual) - if isinstance(node, Ephemeral)] + if isinstance(type(node), MetaEphemeral)] if len(ephemerals_idx) > 0: if mode == "one": @@ -867,8 +876,8 @@ def staticLimit(key, max_value): depth), because it can ensure that no tree higher than this limit will ever be accepted in the population, except if it was generated at initialization time. - - :param key: The function to use in order the get the wanted value. For + + :param key: The function to use in order the get the wanted value. For instance, on a GP tree, ``operator.attrgetter('height')`` may be used to set a depth limit, and ``len`` to set a size limit. :param max_value: The maximum value allowed for the given measurement. From 77359b9ed1cf93e3c59e7f073822f36bdc7936d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Sat, 2 May 2015 17:29:38 -0400 Subject: [PATCH 04/26] Fix ephemeral pickling test. lambda can't pickled and will never be picklable. The test was failing because of Python incapicity to pickle lambdas, not because of MetaEphemeral logic. Therefore, I am replacing lambda by a partial and plan to do it for every examples in GP. --- deap/tests/test_pickle.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deap/tests/test_pickle.py b/deap/tests/test_pickle.py index 6c5b843af..3828a574a 100644 --- a/deap/tests/test_pickle.py +++ b/deap/tests/test_pickle.py @@ -5,6 +5,7 @@ import pickle import operator import platform +import functools import numpy @@ -99,7 +100,7 @@ def test_pickle_tree_term(self): def test_pickle_tree_ephemeral(self): pset = gp.PrimitiveSetTyped("MAIN", [], int, "IN") pset.addPrimitive(operator.add, [int, int], int) - pset.addEphemeralConstant("E1", lambda: 2, int) + pset.addEphemeralConstant("E1", functools.partial(int, 2), int) expr = gp.genFull(pset, min_=1, max_=1) ind = creator.IndTree(expr) From 70a0b8b2e774e602b3aa1969c8e410166e489f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Sat, 2 May 2015 17:45:43 -0400 Subject: [PATCH 05/26] Add a warning when an ephemeral is created with a lambda. --- deap/gp.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deap/gp.py b/deap/gp.py index b7e01162c..6b9a323d1 100644 --- a/deap/gp.py +++ b/deap/gp.py @@ -25,6 +25,7 @@ import random import re import sys +import types import warnings from collections import defaultdict, deque @@ -242,6 +243,12 @@ def __new__(meta, name, func, ret=__type__, id_=None): if id_ in MetaEphemeral.cache: return MetaEphemeral.cache[id_] + if isinstance(func, types.LambdaType) and func.__name__ == '': + warnings.warn("Ephemeral {name} function cannot be " + "pickled because its generating function " + "is a lambda function. Use functools.partial " + "instead.".format(name=name), RuntimeWarning) + def __init__(self): self.value = func() From 185d60095e2d294a174191599127e229533ad06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix-Antoine=20Fortin?= Date: Sat, 2 May 2015 19:01:25 -0400 Subject: [PATCH 06/26] Replace lambda by partial in GP examples. --- examples/gp/adf_symbreg.py | 4 +++- examples/gp/spambase.py | 4 +++- examples/gp/symbreg.py | 4 +++- examples/gp/symbreg_numpy.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/gp/adf_symbreg.py b/examples/gp/adf_symbreg.py index d5fd188b8..7574761ff 100644 --- a/examples/gp/adf_symbreg.py +++ b/examples/gp/adf_symbreg.py @@ -19,6 +19,8 @@ import numpy +from functools import partial + from deap import base from deap import creator from deap import gp @@ -69,7 +71,7 @@ def protectedDiv(left, right): pset.addPrimitive(operator.neg, 1) pset.addPrimitive(math.cos, 1) pset.addPrimitive(math.sin, 1) -pset.addEphemeralConstant("rand101", lambda: random.randint(-1, 1)) +pset.addEphemeralConstant("rand101", partial(random.randint, -1, 1)) pset.addADF(adfset0) pset.addADF(adfset1) pset.addADF(adfset2) diff --git a/examples/gp/spambase.py b/examples/gp/spambase.py index a9b8d59c0..c45ce07a4 100644 --- a/examples/gp/spambase.py +++ b/examples/gp/spambase.py @@ -20,6 +20,8 @@ import numpy +from functools import partial + from deap import algorithms from deap import base from deap import creator @@ -64,7 +66,7 @@ def if_then_else(input, output1, output2): pset.addPrimitive(if_then_else, [bool, float, float], float) # terminals -pset.addEphemeralConstant("rand100", lambda: random.random() * 100, float) +pset.addEphemeralConstant("rand100", partial(random.uniform, 0, 100), float) pset.addTerminal(False, bool) pset.addTerminal(True, bool) diff --git a/examples/gp/symbreg.py b/examples/gp/symbreg.py index 3aada54db..764bc98b4 100644 --- a/examples/gp/symbreg.py +++ b/examples/gp/symbreg.py @@ -19,6 +19,8 @@ import numpy +from functools import partial + from deap import algorithms from deap import base from deap import creator @@ -40,7 +42,7 @@ def protectedDiv(left, right): pset.addPrimitive(operator.neg, 1) pset.addPrimitive(math.cos, 1) pset.addPrimitive(math.sin, 1) -pset.addEphemeralConstant("rand101", lambda: random.randint(-1,1)) +pset.addEphemeralConstant("rand101", partial(random.randint, -1, 1)) pset.renameArguments(ARG0='x') creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) diff --git a/examples/gp/symbreg_numpy.py b/examples/gp/symbreg_numpy.py index f3d0054bd..327850116 100644 --- a/examples/gp/symbreg_numpy.py +++ b/examples/gp/symbreg_numpy.py @@ -19,6 +19,8 @@ import numpy +from functools import partial + from deap import algorithms from deap import base from deap import creator @@ -44,7 +46,7 @@ def protectedDiv(left, right): pset.addPrimitive(numpy.negative, 1, name="vneg") pset.addPrimitive(numpy.cos, 1, name="vcos") pset.addPrimitive(numpy.sin, 1, name="vsin") -pset.addEphemeralConstant("rand101", lambda: random.randint(-1,1)) +pset.addEphemeralConstant("rand101", partial(random.randint, -1, 1)) pset.renameArguments(ARG0='x') creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) From 14b7e357f833352398eab8d8a52a58450efe9e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 09:59:16 -0500 Subject: [PATCH 07/26] Started switching to full azure-pipelines --- .../cd.yml | 0 .azure-pipelines/ci.yml | 35 ++++++++++++ .travis.yml | 56 ------------------- 3 files changed, 35 insertions(+), 56 deletions(-) rename azure-pipelines.yml => .azure-pipelines/cd.yml (100%) create mode 100644 .azure-pipelines/ci.yml delete mode 100644 .travis.yml diff --git a/azure-pipelines.yml b/.azure-pipelines/cd.yml similarity index 100% rename from azure-pipelines.yml rename to .azure-pipelines/cd.yml diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml new file mode 100644 index 000000000..3cc46cfd0 --- /dev/null +++ b/.azure-pipelines/ci.yml @@ -0,0 +1,35 @@ +trigger: +- master +- dev + +pool: + vmImage: 'ubuntu-16.04' +strategy: + matrix: + Python37: + python.version: '3.7' + Python38: + python.version: '3.8' + Python39: + python.version: '3.9' + +steps: +- task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + displayName: 'Use Python $(python.version)' + +- script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + displayName: 'Install dependencies' + +- script: | + python setup.py install + displayName: 'Install library' + +- script: | + pip install pytest pytest-azurepipelines pytest-cov + pytest + displayName: 'Run tests' + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 10514ea0f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -sudo: false -language: python -branches: - only: - - master - - devel - -matrix: - include: - - python: "2.7" - env: NUMPY=numpy - - python: "3.4" - env: NUMPY=numpy - - python: "3.5" - env: NUMPY=numpy - - python: "3.6" - env: NUMPY=numpy - - python: "3.7" - dist: xenial - env: NUMPY=numpy - - python: "pypy" - env: NUMPY="git+https://bitbucket.org/pypy/numpy.git" -addons: - apt: - packages: - - gfortran - - libblas-dev - - liblapack-dev -# command to install dependencies and translate deap in py3k -install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - cd "$PYENV_ROOT" && git pull - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="4.0.1" - "$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" - virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" - source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" - NUMPY=$NUMPY"@pypy-4.0.1" - pip install nose - fi - - pip install $NUMPY -# command to run tests -script: - - python setup.py nosetests --verbosity=3 --with-doctest -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/f17745fc250ffb09c0e9 - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: false # default: false From 739e44911dc8d7a46ded73c5ffbaaa9b312f04ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 10:04:16 -0500 Subject: [PATCH 08/26] Removed requirements.txt step --- .azure-pipelines/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 3cc46cfd0..be07835c4 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -19,11 +19,6 @@ steps: versionSpec: '$(python.version)' displayName: 'Use Python $(python.version)' -- script: | - python -m pip install --upgrade pip - pip install -r requirements.txt - displayName: 'Install dependencies' - - script: | python setup.py install displayName: 'Install library' From f695ff643023716b0fa5595873d6afd0e57d0bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 10:13:31 -0500 Subject: [PATCH 09/26] Move tests out of source --- deap/tests/deap.rc | 237 ------------ {deap/tests => tests}/__init__.py | 0 {deap/tests => tests}/test_algorithms.py | 0 {deap/tests => tests}/test_benchmarks.py | 0 tests/test_convergence.py | 440 +++++++++++++++++++++++ {deap/tests => tests}/test_creator.py | 0 {deap/tests => tests}/test_init.py | 0 {deap/tests => tests}/test_logbook.py | 0 tests/test_multiproc.py | 26 ++ {deap/tests => tests}/test_mutation.py | 0 tests/test_operators.py | 39 ++ {deap/tests => tests}/test_pickle.py | 0 {deap/tests => tests}/test_statistics.py | 0 13 files changed, 505 insertions(+), 237 deletions(-) delete mode 100644 deap/tests/deap.rc rename {deap/tests => tests}/__init__.py (100%) rename {deap/tests => tests}/test_algorithms.py (100%) rename {deap/tests => tests}/test_benchmarks.py (100%) create mode 100644 tests/test_convergence.py rename {deap/tests => tests}/test_creator.py (100%) rename {deap/tests => tests}/test_init.py (100%) rename {deap/tests => tests}/test_logbook.py (100%) create mode 100644 tests/test_multiproc.py rename {deap/tests => tests}/test_mutation.py (100%) create mode 100644 tests/test_operators.py rename {deap/tests => tests}/test_pickle.py (100%) rename {deap/tests => tests}/test_statistics.py (100%) diff --git a/deap/tests/deap.rc b/deap/tests/deap.rc deleted file mode 100644 index d2f670eca..000000000 --- a/deap/tests/deap.rc +++ /dev/null @@ -1,237 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Profiled execution. -profile=no - -# Add to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=CVS - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -disable=W0142,W0105,R0201 - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=no - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - - -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# List of builtins function names that should not be used, separated by a comma -bad-functions=filter,apply,input - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=(([a-z][a-zA-Z0-9]{1,30})|(__[a-z][a-zA-Z0-9]{1,30}__)|(_[a-z][a-zA-Z0-9]{1,30}))$ - -# Regular expression which should only match correct method names -method-rgx=(([a-z][a-zA-Z0-9]{1,30})|(__[a-z][a-zA-Z0-9]{1,30}__)|(_[a-z][a-zA-Z0-9]{1,30}))$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,_,n,N,t,g - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=((__.*__)|(_.*))|(get.*)|(set.*)|(del.*) - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=2000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|args|kargs|memo - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes= - -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. -generated-members= - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=10 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=30 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=0 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=25 - - -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods= - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp diff --git a/deap/tests/__init__.py b/tests/__init__.py similarity index 100% rename from deap/tests/__init__.py rename to tests/__init__.py diff --git a/deap/tests/test_algorithms.py b/tests/test_algorithms.py similarity index 100% rename from deap/tests/test_algorithms.py rename to tests/test_algorithms.py diff --git a/deap/tests/test_benchmarks.py b/tests/test_benchmarks.py similarity index 100% rename from deap/tests/test_benchmarks.py rename to tests/test_benchmarks.py diff --git a/tests/test_convergence.py b/tests/test_convergence.py new file mode 100644 index 000000000..83841500e --- /dev/null +++ b/tests/test_convergence.py @@ -0,0 +1,440 @@ +# This file is part of DEAP. +# +# DEAP is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# DEAP is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with DEAP. If not, see . + +from itertools import islice +import random + +from nose import with_setup +import numpy + +from deap import algorithms +from deap import base +from deap import benchmarks +from deap.benchmarks.tools import hypervolume +from deap import cma +from deap import creator +from deap import tools + +FITCLSNAME = "FIT_TYPE" +INDCLSNAME = "IND_TYPE" + +HV_THRESHOLD = 116.0 # 120.777 is Optimal value + + +def setup_func_single_obj(): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0,)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + +def setup_func_single_obj_const(): + creator.create(FITCLSNAME, base.ConstrainedFitness, weights=(-1.0,)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + +def setup_func_multi_obj(): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + +def setup_func_multi_obj_numpy(): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) + creator.create(INDCLSNAME, numpy.ndarray, fitness=creator.__dict__[FITCLSNAME]) + +def teardown_func(): + # Messy way to remove a class from the creator + del creator.__dict__[FITCLSNAME] + del creator.__dict__[INDCLSNAME] + +@with_setup(setup_func_single_obj, teardown_func) +def test_cma(): + NDIM = 5 + NGEN = 100 + + strategy = cma.BasicStrategy(centroid=[0.0]*NDIM, sigma=1.0) + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + # Consume the algorithm until NGEN + state = next(islice(algorithms.GenerateUpdateAlgorithm(toolbox), NGEN, None)) + best, = tools.selBest(state.population, k=1) + + assert best.fitness.values < (1e-8,), "CMA algorithm did not converged properly." + +@with_setup(setup_func_multi_obj, teardown_func) +def test_nsga2(): + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU = 16 + NGEN = 100 + + toolbox = base.Toolbox() + toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) + toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) + toolbox.register("population", tools.initRepeat, list, toolbox.individual) + + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) + toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) + toolbox.register("select", tools.selNSGA2) + + pop = toolbox.population(n=MU) + fitnesses = toolbox.map(toolbox.evaluate, pop) + for ind, fit in zip(pop, fitnesses): + ind.fitness.values = fit + + pop = toolbox.select(pop, len(pop)) + for gen in range(1, NGEN): + offspring = tools.selTournamentDCD(pop, len(pop)) + offspring = [toolbox.clone(ind) for ind in offspring] + + for ind1, ind2 in zip(offspring[::2], offspring[1::2]): + if random.random() <= 0.9: + toolbox.mate(ind1, ind2) + + toolbox.mutate(ind1) + toolbox.mutate(ind2) + del ind1.fitness.values, ind2.fitness.values + + invalid_ind = [ind for ind in offspring if not ind.fitness.valid] + fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) + for ind, fit in zip(invalid_ind, fitnesses): + ind.fitness.values = fit + + pop = toolbox.select(pop + offspring, MU) + + hv = hypervolume(pop, [11.0, 11.0]) + # hv = 120.777 # Optimal value + + assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) + + for ind in pop: + assert not (any(numpy.asarray(ind) < BOUND_LOW) or any(numpy.asarray(ind) > BOUND_UP)) + + +@with_setup(setup_func_multi_obj_numpy, teardown_func) +def test_mo_cma_es(): + + def distance(feasible_ind, original_ind): + """A distance function to the feasibility region.""" + return sum((f - o)**2 for f, o in zip(feasible_ind, original_ind)) + + def closest_feasible(individual): + """A function returning a valid individual from an invalid one.""" + feasible_ind = numpy.array(individual) + feasible_ind = numpy.maximum(BOUND_LOW, feasible_ind) + feasible_ind = numpy.minimum(BOUND_UP, feasible_ind) + return feasible_ind + + def valid(individual): + """Determines if the individual is valid or not.""" + if any(individual < BOUND_LOW) or any(individual > BOUND_UP): + return False + return True + + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU, LAMBDA = 10, 10 + NGEN = 500 + + numpy.random.seed(128) + + # The MO-CMA-ES algorithm takes a full population as argument + population = [creator.__dict__[INDCLSNAME](x) for x in numpy.random.uniform(BOUND_LOW, BOUND_UP, (MU, NDIM))] + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.decorate("evaluate", tools.ClosestValidPenalty(valid, closest_feasible, 1.0e+6, distance)) + + for ind in population: + ind.fitness.values = toolbox.evaluate(ind) + + strategy = cma.MultiObjectiveStrategy(population, sigma=1.0, mu=MU, lambda_=LAMBDA) + + toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() + + # Evaluate the individuals + fitnesses = toolbox.map(toolbox.evaluate, population) + for ind, fit in zip(population, fitnesses): + ind.fitness.values = fit + + # Update the strategy with the evaluated individuals + toolbox.update(population) + + # Note that we use a penalty to guide the search to feasible solutions, + # but there is no guarantee that individuals are valid. + # We expect the best individuals will be within bounds or very close. + num_valid = 0 + for ind in strategy.parents: + dist = distance(closest_feasible(ind), ind) + if numpy.isclose(dist, 0.0, rtol=1.e-5, atol=1.e-5): + num_valid += 1 + assert num_valid >= len(strategy.parents) + + # Note that NGEN=500 is enough to get consistent hypervolume > 116, + # but not 119. More generations would help but would slow down testing. + hv = hypervolume(strategy.parents, [11.0, 11.0]) + assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) + + +@with_setup(setup_func_multi_obj, teardown_func) +def test_nsga3(): + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU = 16 + NGEN = 100 + + ref_points = tools.uniform_reference_points(2, p=12) + + toolbox = base.Toolbox() + toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) + toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) + toolbox.register("population", tools.initRepeat, list, toolbox.individual) + + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) + toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) + toolbox.register("select", tools.selNSGA3, ref_points=ref_points) + + pop = toolbox.population(n=MU) + fitnesses = toolbox.map(toolbox.evaluate, pop) + for ind, fit in zip(pop, fitnesses): + ind.fitness.values = fit + + pop = toolbox.select(pop, len(pop)) + # Begin the generational process + for gen in range(1, NGEN): + # Vary the individuals + offspring = list(islice(algorithms.and_variation(pop, toolbox, 1.0, 1.0), len(pop))) + + # Evaluate the individuals with an invalid fitness + invalid_ind = [ind for ind in offspring if not ind.fitness.valid] + fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) + for ind, fit in zip(invalid_ind, fitnesses): + ind.fitness.values = fit + + # Select the next generation population + pop = toolbox.select(pop + offspring, MU) + + hv = hypervolume(pop, [11.0, 11.0]) + # hv = 120.777 # Optimal value + + assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) + + for ind in pop: + assert not (any(numpy.asarray(ind) < BOUND_LOW) or any(numpy.asarray(ind) > BOUND_UP)) + + +@with_setup(setup_func_single_obj, teardown_func) +def test_cma_mixed_integer_1_p_1_no_constraint(): + N = 3 + NGEN = 15000 + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + + parent = (numpy.random.rand(N) * 2) + 1 + + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=1) + + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + best = None + + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() + + # Evaluate the individuals + for individual in population: + individual.fitness.values = toolbox.evaluate(individual) + + if best is None or individual.fitness >= best.fitness: + best = individual + + # We must stop CMA-ES before the update becomes unstable + if best.fitness.values[0] < 1e-12: + break + + # Update the strategy with the evaluated individuals + toolbox.update(population) + + assert best.fitness.values[0] < 1e-12 + + +@with_setup(setup_func_single_obj, teardown_func) +def test_cma_mixed_integer_1_p_20_no_constraint(): + N = 3 + NGEN = 15000 + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + + parent = (numpy.random.rand(N) * 2) + 1 + + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=20) + + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + best = None + + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() + + # Evaluate the individuals + for individual in population: + individual.fitness.values = toolbox.evaluate(individual) + + if best is None or individual.fitness >= best.fitness: + best = individual + + # Stop when we've reached some kind of optimum + if best.fitness.values[0] < 1e-12: + break + + # Update the strategy with the evaluated individuals + toolbox.update(population) + + assert best.fitness.values[0] < 1e-12 + + +@with_setup(setup_func_single_obj_const, teardown_func) +def test_cma_mixed_integer_1_p_1_with_constraint(): + def c1(individual): + if individual[0] + individual[1] < 0.1: + return True + return False + + def c2(individual): + if individual[1] < 0.1: + return True + return False + + N = 5 + NGEN = 15000 + optimum = 0.015 + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + restarts = 10 + + # Allow a couple of restarts + while restarts > 0: + parent = (numpy.random.rand(N) * 2) + 1 + + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=1) + + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + best = None + + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() + + # Evaluate the individuals + for individual in population: + constraint_violation = c1(individual), c2(individual) + if not any(constraint_violation): + individual.fitness.values = toolbox.evaluate(individual) + individual.fitness.constraint_violation = constraint_violation + + if best is None or individual.fitness >= best.fitness: + best = individual + + # Stop when we've reached some kind of optimum + if best.fitness.values[0] - optimum < 1e-7: + restarts = 0 + break + + # Update the strategy with the evaluated individuals + toolbox.update(population) + + if strategy.condition_number > 10e12: + # We've become unstable + break + + restarts -= 1 + + assert best.fitness.values[0] - optimum < 1e-7 + + +@with_setup(setup_func_single_obj_const, teardown_func) +def test_cma_mixed_integer_1_p_20_with_constraint(): + def c1(individual): + if individual[0] + individual[1] < 0.1: + return True + return False + + def c2(individual): + if individual[3] < 0.1: + return True + return False + + N = 5 + NGEN = 15000 + optimum = 0.015 + + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + restarts = 10 + + # Allow a couple of restarts + while restarts > 0: + parent = (numpy.random.rand(N) * 2) + 1 + + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=20) + + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + + best = None + + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() + + # Evaluate the individuals + for individual in population: + constraint_violation = c1(individual), c2(individual) + if not any(constraint_violation): + individual.fitness.values = toolbox.evaluate(individual) + individual.fitness.constraint_violation = constraint_violation + + if best is None or individual.fitness >= best.fitness: + best = individual + + if best.fitness.values[0] - optimum < 1e-7: + restarts = 0 + break + + # Stop when we've reached some kind of optimum + toolbox.update(population) + + if strategy.condition_number > 10e12: + # We've become unstable + break + + restarts -= 1 + + assert best.fitness.values[0] - optimum < 1e-7 diff --git a/deap/tests/test_creator.py b/tests/test_creator.py similarity index 100% rename from deap/tests/test_creator.py rename to tests/test_creator.py diff --git a/deap/tests/test_init.py b/tests/test_init.py similarity index 100% rename from deap/tests/test_init.py rename to tests/test_init.py diff --git a/deap/tests/test_logbook.py b/tests/test_logbook.py similarity index 100% rename from deap/tests/test_logbook.py rename to tests/test_logbook.py diff --git a/tests/test_multiproc.py b/tests/test_multiproc.py new file mode 100644 index 000000000..19a025f55 --- /dev/null +++ b/tests/test_multiproc.py @@ -0,0 +1,26 @@ +import multiprocessing +import unittest + +from deap import base +from deap import creator + + +def _evalOneMax(individual): + return sum(individual), + + +def test_multiproc(): + creator.create("FitnessMax", base.Fitness, weights=(1.0,)) + creator.create("Individual", list, fitness=creator.FitnessMax) + + toolbox = base.Toolbox() + toolbox.register("evaluate", _evalOneMax) + + # Process Pool of 4 workers + pool = multiprocessing.Pool(processes=4) + toolbox.register("map", pool.map) + + pop = [[1]*20 for _ in range(100)] + fitnesses = toolbox.map(toolbox.evaluate, pop) + for ind, fit in zip(pop, fitnesses): + assert fit == (sum(ind),) \ No newline at end of file diff --git a/deap/tests/test_mutation.py b/tests/test_mutation.py similarity index 100% rename from deap/tests/test_mutation.py rename to tests/test_mutation.py diff --git a/tests/test_operators.py b/tests/test_operators.py new file mode 100644 index 000000000..987242eac --- /dev/null +++ b/tests/test_operators.py @@ -0,0 +1,39 @@ +import unittest +try: + from unittest import mock +except ImportError: + import mock +import random + +from ..tools import crossover + +class TestCxOrdered(unittest.TestCase): + def setUp(self): + pass + + def test_crossover(self): + a = [8, 7, 3, 4, 5, 6, 0, 2, 1, 9] + b = [7, 6, 0, 1, 2, 9, 8, 4, 3, 5] + expected_ap = [4, 5, 6, 1, 2, 9, 0, 8, 7, 3] + expected_bp = [1, 2, 9, 4, 5, 6, 8, 3, 7, 0] + + with mock.patch("random.sample", return_value=[3, 5]): + ap, bp = crossover.cxOrdered(a, b) + + self.assertSequenceEqual(expected_ap, ap) + self.assertSequenceEqual(expected_bp, bp) + + + def test_crossover_identical(self): + i1 = list(range(100)) + random.shuffle(i1) + i2 = list(range(100)) + random.shuffle(i2) + + a, b = sorted(random.sample(range(len(i1)), 2)) + + with mock.patch("random.sample", return_value=[a, b]): + ap, bp = crossover.cxOrdered(i1, i2) + + self.assertSequenceEqual(sorted(ap), list(range(len(ap)))) + self.assertSequenceEqual(sorted(bp), list(range(len(bp)))) diff --git a/deap/tests/test_pickle.py b/tests/test_pickle.py similarity index 100% rename from deap/tests/test_pickle.py rename to tests/test_pickle.py diff --git a/deap/tests/test_statistics.py b/tests/test_statistics.py similarity index 100% rename from deap/tests/test_statistics.py rename to tests/test_statistics.py From 0e1c2c1b97919545c3b075b2f4a9fa6b9f74276a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 12:48:53 -0500 Subject: [PATCH 10/26] Fixed pytest testing --- .azure-pipelines/ci.yml | 8 ++++++-- setup.py | 2 +- tests/__init__.py | 14 -------------- tests/test_operators.py | 2 +- 4 files changed, 8 insertions(+), 18 deletions(-) delete mode 100644 tests/__init__.py diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index be07835c4..0ec1eb0b8 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -20,11 +20,15 @@ steps: displayName: 'Use Python $(python.version)' - script: | - python setup.py install + python -m pip install --upgrade pip wheel + displayName: 'Install build tools' + +- script: | + pip install . displayName: 'Install library' - script: | pip install pytest pytest-azurepipelines pytest-cov - pytest + pytest . displayName: 'Run tests' diff --git a/setup.py b/setup.py index 82276cd80..c5b9259eb 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ def run_setup(build_ext): author='deap Development Team', author_email='deap-users@googlegroups.com', url='https://www.github.com/deap', - packages=find_packages(exclude=['examples']), + packages=find_packages(exclude=['examples', 'tests']), # packages=['deap', 'deap.tools', 'deap.tools._hypervolume', 'deap.benchmarks', 'deap.tests'], platforms=['any'], keywords=['evolutionary algorithms', 'genetic algorithms', 'genetic programming', 'cma-es', 'ga', 'gp', 'es', 'pso'], diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index b5c591437..000000000 --- a/tests/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# This file is part of DEAP. -# -# DEAP is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# DEAP is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with DEAP. If not, see . diff --git a/tests/test_operators.py b/tests/test_operators.py index 987242eac..d4d0ba24e 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -5,7 +5,7 @@ import mock import random -from ..tools import crossover +from deap.tools import crossover class TestCxOrdered(unittest.TestCase): def setUp(self): From 949462a6fbf570cac0d0d25a763ef8be7c615624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 12:52:44 -0500 Subject: [PATCH 11/26] Fixed typo in pipeline --- .azure-pipelines/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 0ec1eb0b8..fc07ffdfd 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -20,8 +20,8 @@ steps: displayName: 'Use Python $(python.version)' - script: | - python -m pip install --upgrade pip wheel - displayName: 'Install build tools' + python -m pip install --upgrade pip wheel + displayName: 'Install build tools' - script: | pip install . From 59f81e4aa90e5adb061768115bd60c0785f6dfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 13:41:14 -0500 Subject: [PATCH 12/26] Removed nose and fixed tests --- tests/test_convergence.py | 608 +++++++++++++++++++------------------- tests/test_creator.py | 104 +++---- 2 files changed, 354 insertions(+), 358 deletions(-) diff --git a/tests/test_convergence.py b/tests/test_convergence.py index 83841500e..1d1856d58 100644 --- a/tests/test_convergence.py +++ b/tests/test_convergence.py @@ -15,9 +15,12 @@ from itertools import islice import random +import unittest -from nose import with_setup -import numpy +try: + import numpy +except ImportError: + numpy = False from deap import algorithms from deap import base @@ -33,408 +36,407 @@ HV_THRESHOLD = 116.0 # 120.777 is Optimal value -def setup_func_single_obj(): - creator.create(FITCLSNAME, base.Fitness, weights=(-1.0,)) - creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) +class TearDownCreatorTestCase(unittest.TestCase): + def tearDown(self): + # Messy way to remove a class from the creator + del creator.__dict__[FITCLSNAME] + del creator.__dict__[INDCLSNAME] -def setup_func_single_obj_const(): - creator.create(FITCLSNAME, base.ConstrainedFitness, weights=(-1.0,)) - creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) +class TestSingleObjective(TearDownCreatorTestCase): + def setUp(self): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0,)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) -def setup_func_multi_obj(): - creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) - creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + def test_cma(self): + NDIM = 5 + NGEN = 100 -def setup_func_multi_obj_numpy(): - creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) - creator.create(INDCLSNAME, numpy.ndarray, fitness=creator.__dict__[FITCLSNAME]) + strategy = cma.BasicStrategy(centroid=[0.0]*NDIM, sigma=1.0) -def teardown_func(): - # Messy way to remove a class from the creator - del creator.__dict__[FITCLSNAME] - del creator.__dict__[INDCLSNAME] + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) -@with_setup(setup_func_single_obj, teardown_func) -def test_cma(): - NDIM = 5 - NGEN = 100 + # Consume the algorithm until NGEN + state = next(islice(algorithms.GenerateUpdateAlgorithm(toolbox), NGEN, None)) + best, = tools.selBest(state.population, k=1) - strategy = cma.BasicStrategy(centroid=[0.0]*NDIM, sigma=1.0) + self.assertLess(best.fitness.values[0], 1e-8) - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.sphere) - toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) + def test_cma_mixed_integer_1_p_1_no_constraint(self): + N = 3 + NGEN = 15000 - # Consume the algorithm until NGEN - state = next(islice(algorithms.GenerateUpdateAlgorithm(toolbox), NGEN, None)) - best, = tools.selBest(state.population, k=1) + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) - assert best.fitness.values < (1e-8,), "CMA algorithm did not converged properly." + parent = (numpy.random.rand(N) * 2) + 1 -@with_setup(setup_func_multi_obj, teardown_func) -def test_nsga2(): - NDIM = 5 - BOUND_LOW, BOUND_UP = 0.0, 1.0 - MU = 16 - NGEN = 100 + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=1) - toolbox = base.Toolbox() - toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) - toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) - toolbox.register("population", tools.initRepeat, list, toolbox.individual) + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) - toolbox.register("evaluate", benchmarks.zdt1) - toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) - toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) - toolbox.register("select", tools.selNSGA2) + best = None - pop = toolbox.population(n=MU) - fitnesses = toolbox.map(toolbox.evaluate, pop) - for ind, fit in zip(pop, fitnesses): - ind.fitness.values = fit + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() - pop = toolbox.select(pop, len(pop)) - for gen in range(1, NGEN): - offspring = tools.selTournamentDCD(pop, len(pop)) - offspring = [toolbox.clone(ind) for ind in offspring] + # Evaluate the individuals + for individual in population: + individual.fitness.values = toolbox.evaluate(individual) - for ind1, ind2 in zip(offspring[::2], offspring[1::2]): - if random.random() <= 0.9: - toolbox.mate(ind1, ind2) + if best is None or individual.fitness >= best.fitness: + best = individual - toolbox.mutate(ind1) - toolbox.mutate(ind2) - del ind1.fitness.values, ind2.fitness.values + # We must stop CMA-ES before the update becomes unstable + if best.fitness.values[0] < 1e-12: + break - invalid_ind = [ind for ind in offspring if not ind.fitness.valid] - fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) - for ind, fit in zip(invalid_ind, fitnesses): - ind.fitness.values = fit + # Update the strategy with the evaluated individuals + toolbox.update(population) - pop = toolbox.select(pop + offspring, MU) + self.assertLess(best.fitness.values[0], 1e-12) - hv = hypervolume(pop, [11.0, 11.0]) - # hv = 120.777 # Optimal value + def test_cma_mixed_integer_1_p_20_no_constraint(self): + N = 3 + NGEN = 15000 - assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) - for ind in pop: - assert not (any(numpy.asarray(ind) < BOUND_LOW) or any(numpy.asarray(ind) > BOUND_UP)) + parent = (numpy.random.rand(N) * 2) + 1 + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=20) -@with_setup(setup_func_multi_obj_numpy, teardown_func) -def test_mo_cma_es(): + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) - def distance(feasible_ind, original_ind): - """A distance function to the feasibility region.""" - return sum((f - o)**2 for f, o in zip(feasible_ind, original_ind)) + best = None - def closest_feasible(individual): - """A function returning a valid individual from an invalid one.""" - feasible_ind = numpy.array(individual) - feasible_ind = numpy.maximum(BOUND_LOW, feasible_ind) - feasible_ind = numpy.minimum(BOUND_UP, feasible_ind) - return feasible_ind + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() - def valid(individual): - """Determines if the individual is valid or not.""" - if any(individual < BOUND_LOW) or any(individual > BOUND_UP): - return False - return True + # Evaluate the individuals + for individual in population: + individual.fitness.values = toolbox.evaluate(individual) - NDIM = 5 - BOUND_LOW, BOUND_UP = 0.0, 1.0 - MU, LAMBDA = 10, 10 - NGEN = 500 + if best is None or individual.fitness >= best.fitness: + best = individual - numpy.random.seed(128) + # Stop when we've reached some kind of optimum + if best.fitness.values[0] < 1e-12: + break - # The MO-CMA-ES algorithm takes a full population as argument - population = [creator.__dict__[INDCLSNAME](x) for x in numpy.random.uniform(BOUND_LOW, BOUND_UP, (MU, NDIM))] + # Update the strategy with the evaluated individuals + toolbox.update(population) - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.zdt1) - toolbox.decorate("evaluate", tools.ClosestValidPenalty(valid, closest_feasible, 1.0e+6, distance)) + self.assertLess(best.fitness.values[0], 1e-12) - for ind in population: - ind.fitness.values = toolbox.evaluate(ind) - strategy = cma.MultiObjectiveStrategy(population, sigma=1.0, mu=MU, lambda_=LAMBDA) +class TestSingleObjectiveConstrained(TearDownCreatorTestCase): + def setUp(self): + creator.create(FITCLSNAME, base.ConstrainedFitness, weights=(-1.0,)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) - toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) - for gen in range(NGEN): - # Generate a new population - population = toolbox.generate() + def test_cma_mixed_integer_1_p_1_with_constraint(self): + def c1(individual): + if individual[0] + individual[1] < 0.1: + return True + return False - # Evaluate the individuals - fitnesses = toolbox.map(toolbox.evaluate, population) - for ind, fit in zip(population, fitnesses): - ind.fitness.values = fit + def c2(individual): + if individual[1] < 0.1: + return True + return False - # Update the strategy with the evaluated individuals - toolbox.update(population) - - # Note that we use a penalty to guide the search to feasible solutions, - # but there is no guarantee that individuals are valid. - # We expect the best individuals will be within bounds or very close. - num_valid = 0 - for ind in strategy.parents: - dist = distance(closest_feasible(ind), ind) - if numpy.isclose(dist, 0.0, rtol=1.e-5, atol=1.e-5): - num_valid += 1 - assert num_valid >= len(strategy.parents) - - # Note that NGEN=500 is enough to get consistent hypervolume > 116, - # but not 119. More generations would help but would slow down testing. - hv = hypervolume(strategy.parents, [11.0, 11.0]) - assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) - - -@with_setup(setup_func_multi_obj, teardown_func) -def test_nsga3(): - NDIM = 5 - BOUND_LOW, BOUND_UP = 0.0, 1.0 - MU = 16 - NGEN = 100 - - ref_points = tools.uniform_reference_points(2, p=12) - - toolbox = base.Toolbox() - toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) - toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) - toolbox.register("population", tools.initRepeat, list, toolbox.individual) - - toolbox.register("evaluate", benchmarks.zdt1) - toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) - toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) - toolbox.register("select", tools.selNSGA3, ref_points=ref_points) - - pop = toolbox.population(n=MU) - fitnesses = toolbox.map(toolbox.evaluate, pop) - for ind, fit in zip(pop, fitnesses): - ind.fitness.values = fit - - pop = toolbox.select(pop, len(pop)) - # Begin the generational process - for gen in range(1, NGEN): - # Vary the individuals - offspring = list(islice(algorithms.and_variation(pop, toolbox, 1.0, 1.0), len(pop))) - - # Evaluate the individuals with an invalid fitness - invalid_ind = [ind for ind in offspring if not ind.fitness.valid] - fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) - for ind, fit in zip(invalid_ind, fitnesses): - ind.fitness.values = fit + N = 5 + NGEN = 15000 + optimum = 0.015 - # Select the next generation population - pop = toolbox.select(pop + offspring, MU) + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + restarts = 10 - hv = hypervolume(pop, [11.0, 11.0]) - # hv = 120.777 # Optimal value + # Allow a couple of restarts + while restarts > 0: + parent = (numpy.random.rand(N) * 2) + 1 - assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=1) - for ind in pop: - assert not (any(numpy.asarray(ind) < BOUND_LOW) or any(numpy.asarray(ind) > BOUND_UP)) + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) + best = None -@with_setup(setup_func_single_obj, teardown_func) -def test_cma_mixed_integer_1_p_1_no_constraint(): - N = 3 - NGEN = 15000 + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.sphere) + # Evaluate the individuals + for individual in population: + constraint_violation = c1(individual), c2(individual) + if not any(constraint_violation): + individual.fitness.values = toolbox.evaluate(individual) + individual.fitness.constraint_violation = constraint_violation - parent = (numpy.random.rand(N) * 2) + 1 + if best is None or individual.fitness >= best.fitness: + best = individual - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=1) + # Stop when we've reached some kind of optimum + if best.fitness.values[0] - optimum < 1e-7: + restarts = 0 + break - toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) + # Update the strategy with the evaluated individuals + toolbox.update(population) - best = None + if strategy.condition_number > 10e12: + # We've become unstable + break - for gen in range(NGEN): - # Generate a new population - population = toolbox.generate() + restarts -= 1 - # Evaluate the individuals - for individual in population: - individual.fitness.values = toolbox.evaluate(individual) + self.assertLess(best.fitness.values[0] - optimum, 1e-7) - if best is None or individual.fitness >= best.fitness: - best = individual + def test_cma_mixed_integer_1_p_20_with_constraint(self): + def c1(individual): + if individual[0] + individual[1] < 0.1: + return True + return False - # We must stop CMA-ES before the update becomes unstable - if best.fitness.values[0] < 1e-12: - break + def c2(individual): + if individual[3] < 0.1: + return True + return False - # Update the strategy with the evaluated individuals - toolbox.update(population) + N = 5 + NGEN = 15000 + optimum = 0.015 - assert best.fitness.values[0] < 1e-12 + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.sphere) + restarts = 10 + # Allow a couple of restarts + while restarts > 0: + parent = (numpy.random.rand(N) * 2) + 1 -@with_setup(setup_func_single_obj, teardown_func) -def test_cma_mixed_integer_1_p_20_no_constraint(): - N = 3 - NGEN = 15000 + strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=20) - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.sphere) + toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) - parent = (numpy.random.rand(N) * 2) + 1 + best = None - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=20) + for gen in range(NGEN): + # Generate a new population + population = toolbox.generate() - toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) + # Evaluate the individuals + for individual in population: + constraint_violation = c1(individual), c2(individual) + if not any(constraint_violation): + individual.fitness.values = toolbox.evaluate(individual) + individual.fitness.constraint_violation = constraint_violation - best = None + if best is None or individual.fitness >= best.fitness: + best = individual - for gen in range(NGEN): - # Generate a new population - population = toolbox.generate() + if best.fitness.values[0] - optimum < 1e-7: + restarts = 0 + break - # Evaluate the individuals - for individual in population: - individual.fitness.values = toolbox.evaluate(individual) + # Stop when we've reached some kind of optimum + toolbox.update(population) - if best is None or individual.fitness >= best.fitness: - best = individual + if strategy.condition_number > 10e12: + # We've become unstable + break - # Stop when we've reached some kind of optimum - if best.fitness.values[0] < 1e-12: - break + restarts -= 1 - # Update the strategy with the evaluated individuals - toolbox.update(population) + self.assertLess(best.fitness.values[0] - optimum, 1e-7) - assert best.fitness.values[0] < 1e-12 +class TestMultiObjective(TearDownCreatorTestCase): + def setUp(self): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) + creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) -@with_setup(setup_func_single_obj_const, teardown_func) -def test_cma_mixed_integer_1_p_1_with_constraint(): - def c1(individual): - if individual[0] + individual[1] < 0.1: - return True - return False + def test_nsga2(self): + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU = 16 + NGEN = 100 - def c2(individual): - if individual[1] < 0.1: - return True - return False + toolbox = base.Toolbox() + toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) + toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) + toolbox.register("population", tools.initRepeat, list, toolbox.individual) - N = 5 - NGEN = 15000 - optimum = 0.015 + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) + toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) + toolbox.register("select", tools.selNSGA2) - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.sphere) - restarts = 10 + pop = toolbox.population(n=MU) + fitnesses = toolbox.map(toolbox.evaluate, pop) + for ind, fit in zip(pop, fitnesses): + ind.fitness.values = fit - # Allow a couple of restarts - while restarts > 0: - parent = (numpy.random.rand(N) * 2) + 1 + pop = toolbox.select(pop, len(pop)) + for gen in range(1, NGEN): + offspring = tools.selTournamentDCD(pop, len(pop)) + offspring = [toolbox.clone(ind) for ind in offspring] - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=1) + for ind1, ind2 in zip(offspring[::2], offspring[1::2]): + if random.random() <= 0.9: + toolbox.mate(ind1, ind2) - toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) + toolbox.mutate(ind1) + toolbox.mutate(ind2) + del ind1.fitness.values, ind2.fitness.values - best = None + invalid_ind = [ind for ind in offspring if not ind.fitness.valid] + fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) + for ind, fit in zip(invalid_ind, fitnesses): + ind.fitness.values = fit - for gen in range(NGEN): - # Generate a new population - population = toolbox.generate() + pop = toolbox.select(pop + offspring, MU) - # Evaluate the individuals - for individual in population: - constraint_violation = c1(individual), c2(individual) - if not any(constraint_violation): - individual.fitness.values = toolbox.evaluate(individual) - individual.fitness.constraint_violation = constraint_violation + hv = hypervolume(pop, [11.0, 11.0]) + # hv = 120.777 # Optimal value - if best is None or individual.fitness >= best.fitness: - best = individual + self.assertGreater(hv, HV_THRESHOLD) - # Stop when we've reached some kind of optimum - if best.fitness.values[0] - optimum < 1e-7: - restarts = 0 - break + for ind in pop: + self.assertTrue(all(numpy.asarray(ind) >= BOUND_LOW)) + self.assertTrue(all(numpy.asarray(ind) <= BOUND_UP)) - # Update the strategy with the evaluated individuals - toolbox.update(population) + def test_nsga3(self): + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU = 16 + NGEN = 100 - if strategy.condition_number > 10e12: - # We've become unstable - break + ref_points = tools.uniform_reference_points(2, p=12) - restarts -= 1 + toolbox = base.Toolbox() + toolbox.register("attr_float", random.uniform, BOUND_LOW, BOUND_UP) + toolbox.register("individual", tools.initRepeat, creator.__dict__[INDCLSNAME], toolbox.attr_float, NDIM) + toolbox.register("population", tools.initRepeat, list, toolbox.individual) - assert best.fitness.values[0] - optimum < 1e-7 + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0) + toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM) + toolbox.register("select", tools.selNSGA3, ref_points=ref_points) + pop = toolbox.population(n=MU) + fitnesses = toolbox.map(toolbox.evaluate, pop) + for ind, fit in zip(pop, fitnesses): + ind.fitness.values = fit -@with_setup(setup_func_single_obj_const, teardown_func) -def test_cma_mixed_integer_1_p_20_with_constraint(): - def c1(individual): - if individual[0] + individual[1] < 0.1: + pop = toolbox.select(pop, len(pop)) + # Begin the generational process + for gen in range(1, NGEN): + # Vary the individuals + offspring = list(islice(algorithms.and_variation(pop, toolbox, 1.0, 1.0), len(pop))) + + # Evaluate the individuals with an invalid fitness + invalid_ind = [ind for ind in offspring if not ind.fitness.valid] + fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) + for ind, fit in zip(invalid_ind, fitnesses): + ind.fitness.values = fit + + # Select the next generation population + pop = toolbox.select(pop + offspring, MU) + + hv = hypervolume(pop, [11.0, 11.0]) + # hv = 120.777 # Optimal value + + self.assertGreater(hv, HV_THRESHOLD) + + for ind in pop: + self.assertTrue(all(numpy.asarray(ind) >= BOUND_LOW)) + self.assertTrue(all(numpy.asarray(ind) <= BOUND_UP)) + +@unittest.skipUnless(numpy, "requires numpy") +class TestMultiObjectiveNumpy(TearDownCreatorTestCase): + def setUp(self): + creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) + creator.create(INDCLSNAME, numpy.ndarray, fitness=creator.__dict__[FITCLSNAME]) + + def test_mo_cma_es(self): + + def distance(feasible_ind, original_ind): + """A distance function to the feasibility region.""" + return sum((f - o)**2 for f, o in zip(feasible_ind, original_ind)) + + def closest_feasible(individual): + """A function returning a valid individual from an invalid one.""" + feasible_ind = numpy.array(individual) + feasible_ind = numpy.maximum(BOUND_LOW, feasible_ind) + feasible_ind = numpy.minimum(BOUND_UP, feasible_ind) + return feasible_ind + + def valid(individual): + """Determines if the individual is valid or not.""" + if any(individual < BOUND_LOW) or any(individual > BOUND_UP): + return False return True - return False - def c2(individual): - if individual[3] < 0.1: - return True - return False + NDIM = 5 + BOUND_LOW, BOUND_UP = 0.0, 1.0 + MU, LAMBDA = 10, 10 + NGEN = 500 - N = 5 - NGEN = 15000 - optimum = 0.015 + numpy.random.seed(128) - toolbox = base.Toolbox() - toolbox.register("evaluate", benchmarks.sphere) - restarts = 10 + # The MO-CMA-ES algorithm takes a full population as argument + population = [creator.__dict__[INDCLSNAME](x) for x in numpy.random.uniform(BOUND_LOW, BOUND_UP, (MU, NDIM))] - # Allow a couple of restarts - while restarts > 0: - parent = (numpy.random.rand(N) * 2) + 1 + toolbox = base.Toolbox() + toolbox.register("evaluate", benchmarks.zdt1) + toolbox.decorate("evaluate", tools.ClosestValidPenalty(valid, closest_feasible, 1.0e+6, distance)) - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=20) + for ind in population: + ind.fitness.values = toolbox.evaluate(ind) - toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) - toolbox.register("update", strategy.update) + strategy = cma.MultiObjectiveStrategy(population, sigma=1.0, mu=MU, lambda_=LAMBDA) - best = None + toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) + toolbox.register("update", strategy.update) for gen in range(NGEN): # Generate a new population population = toolbox.generate() # Evaluate the individuals - for individual in population: - constraint_violation = c1(individual), c2(individual) - if not any(constraint_violation): - individual.fitness.values = toolbox.evaluate(individual) - individual.fitness.constraint_violation = constraint_violation + fitnesses = toolbox.map(toolbox.evaluate, population) + for ind, fit in zip(population, fitnesses): + ind.fitness.values = fit - if best is None or individual.fitness >= best.fitness: - best = individual - - if best.fitness.values[0] - optimum < 1e-7: - restarts = 0 - break - - # Stop when we've reached some kind of optimum + # Update the strategy with the evaluated individuals toolbox.update(population) - if strategy.condition_number > 10e12: - # We've become unstable - break + # Note that we use a penalty to guide the search to feasible solutions, + # but there is no guarantee that individuals are valid. + # We expect the best individuals will be within bounds or very close. + num_valid = 0 + for ind in strategy.parents: + dist = distance(closest_feasible(ind), ind) + if numpy.isclose(dist, 0.0, rtol=1.e-5, atol=1.e-5): + num_valid += 1 + self.assertGreaterEqual(num_valid, len(strategy.parents)) + + # Note that NGEN=500 is enough to get consistent hypervolume > 116, + # but not 119. More generations would help but would slow down testing. + hv = hypervolume(strategy.parents, [11.0, 11.0]) + self.assertGreater(hv, HV_THRESHOLD, msg="Hypervolume is lower than expected") - restarts -= 1 - assert best.fitness.values[0] - optimum < 1e-7 diff --git a/tests/test_creator.py b/tests/test_creator.py index a7e3178cd..09ac54319 100644 --- a/tests/test_creator.py +++ b/tests/test_creator.py @@ -13,8 +13,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with DEAP. If not, see . -from nose import with_setup -from unittest import skipIf +import unittest import array @@ -27,57 +26,52 @@ CNAME = "CLASS_NAME" -def teardown_func(): - creator.__dict__.pop(CNAME) -@with_setup(None, teardown_func) -def test_create(): - creator.create(CNAME, list) - l = creator.__dict__[CNAME]([1,2,3,4]) - - assert l == [1,2,3,4], "%s, expected %s" % (l, [1,2,3,4]) - -@with_setup(None, teardown_func) -def test_attribute(): - creator.create(CNAME, list, a=1) - l = creator.__dict__[CNAME]([1,2,3,4]) - - assert l.a == 1, "%s, expected %i" % (l.a, 1) - -@with_setup(None, teardown_func) -def test_array(): - creator.create(CNAME, array.array, typecode="i") - a = creator.__dict__[CNAME]([1,2,3,4]) - b = creator.__dict__[CNAME]([5,6,7,8]) - - a[1:3], b[1:3] = b[1:3], a[1:3] - ta = array.array("i", [1,6,7,4]) - tb = array.array("i", [5,2,3,8]) - assert a == ta, "%s, expected %s" % (a, ta) - assert b == tb, "%s, expected %s" % (b, tb) - -@skipIf(not numpy, "Cannot import Numpy numerical library") -@with_setup(None, teardown_func) -def test_numpy_nocopy(): - creator.create(CNAME, numpy.ndarray) - a = creator.__dict__[CNAME]([1,2,3,4]) - b = creator.__dict__[CNAME]([5,6,7,8]) - - a[1:3], b[1:3] = b[1:3], a[1:3] - ta = numpy.array([1,6,7,4]) - tb = numpy.array([5,6,7,8]) - assert all(a == ta), "%s, expected %s" % (a, ta) - assert all(b == tb), "%s, expected %s" % (b, tb) - -@skipIf(not numpy, "Cannot import Numpy numerical library") -@with_setup(None, teardown_func) -def test_numpy_copy(): - creator.create(CNAME, numpy.ndarray) - a = creator.__dict__[CNAME]([1,2,3,4]) - b = creator.__dict__[CNAME]([5,6,7,8]) - - a[1:3], b[1:3] = b[1:3].copy(), a[1:3].copy() - ta = numpy.array([1,6,7,4]) - tb = numpy.array([5,2,3,8]) - assert all(a == ta), "%s, expected %s" % (a, ta) - assert all(b == tb), "%s, expected %s" % (b, tb) +class TestCreator(unittest.TestCase): + def tearDown(self): + creator.__dict__.pop(CNAME) + + def test_create(self): + creator.create(CNAME, list) + l = creator.__dict__[CNAME]([1, 2, 3, 4]) + self.assertSequenceEqual(l, [1, 2, 3, 4]) + + def test_attribute(self): + creator.create(CNAME, list, a=1) + l = creator.__dict__[CNAME]([1, 2, 3, 4]) + self.assertEqual(l.a, 1) + + def test_array(self): + creator.create(CNAME, array.array, typecode="i") + a = creator.__dict__[CNAME]([1, 2, 3, 4]) + b = creator.__dict__[CNAME]([5, 6, 7, 8]) + + a[1:3], b[1:3] = b[1:3], a[1:3] + ta = array.array("i", [1, 6, 7, 4]) + tb = array.array("i", [5, 2, 3, 8]) + self.assertSequenceEqual(a, ta) + self.assertSequenceEqual(b, tb) + + @unittest.skipIf(not numpy, "Cannot import Numpy numerical library") + def test_numpy_nocopy(self): + creator.create(CNAME, numpy.ndarray) + a = creator.__dict__[CNAME]([1, 2, 3, 4]) + b = creator.__dict__[CNAME]([5, 6, 7, 8]) + + a[1:3], b[1:3] = b[1:3], a[1:3] + ta = numpy.array([1, 6, 7, 4]) + tb = numpy.array([5, 6, 7, 8]) + numpy.testing.assert_array_equal(a, ta) + numpy.testing.assert_array_equal(b, tb) + + @unittest.skipIf(not numpy, "Cannot import Numpy numerical library") + def test_numpy_copy(self): + creator.create(CNAME, numpy.ndarray) + a = creator.__dict__[CNAME]([1, 2, 3, 4]) + b = creator.__dict__[CNAME]([5, 6, 7, 8]) + + a[1:3], b[1:3] = b[1:3].copy(), a[1:3].copy() + ta = numpy.array([1, 6, 7, 4]) + tb = numpy.array([5, 2, 3, 8]) + numpy.testing.assert_array_equal(a, ta) + numpy.testing.assert_array_equal(b, tb) From 977ee688058726c71a639fce0fa061e82add05c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 13:49:22 -0500 Subject: [PATCH 13/26] Add linting step --- .azure-pipelines/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index fc07ffdfd..cc2c26131 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -32,3 +32,7 @@ steps: pytest . displayName: 'Run tests' +- script: | + pip install flake8 + flake8 . + displayName: 'Run linting' From 36e5b20c7ff958b698cef00e5c6ffd0ca6987f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 13:57:32 -0500 Subject: [PATCH 14/26] Add linting pipeline --- .azure-pipelines/linting.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .azure-pipelines/linting.yml diff --git a/.azure-pipelines/linting.yml b/.azure-pipelines/linting.yml new file mode 100644 index 000000000..61f820710 --- /dev/null +++ b/.azure-pipelines/linting.yml @@ -0,0 +1,21 @@ +trigger: +- master +- dev + +steps: +- task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + +- script: | + python -m pip install --upgrade pip wheel + displayName: 'Install build tools' + +- script: | + pip install . + displayName: 'Install library' + +- script: | + pip install flake8 + flake8 . + displayName: 'Run linting' \ No newline at end of file From ee7303ec7267c52441815f7b3110827417d1100a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 14:01:00 -0500 Subject: [PATCH 15/26] Remove linting from CI tests --- .azure-pipelines/ci.yml | 5 ----- .azure-pipelines/linting.yml | 8 -------- 2 files changed, 13 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index cc2c26131..eac6f1a34 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -31,8 +31,3 @@ steps: pip install pytest pytest-azurepipelines pytest-cov pytest . displayName: 'Run tests' - -- script: | - pip install flake8 - flake8 . - displayName: 'Run linting' diff --git a/.azure-pipelines/linting.yml b/.azure-pipelines/linting.yml index 61f820710..b249f5123 100644 --- a/.azure-pipelines/linting.yml +++ b/.azure-pipelines/linting.yml @@ -7,14 +7,6 @@ steps: inputs: versionSpec: '3.9' -- script: | - python -m pip install --upgrade pip wheel - displayName: 'Install build tools' - -- script: | - pip install . - displayName: 'Install library' - - script: | pip install flake8 flake8 . From 649d65fc31e07e28fa19080466e4ee9f98dae1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 14:11:29 -0500 Subject: [PATCH 16/26] Reintegrate in single script --- .azure-pipelines/ci.yml | 65 +++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index eac6f1a34..ef6cf0a75 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -2,32 +2,47 @@ trigger: - master - dev -pool: - vmImage: 'ubuntu-16.04' -strategy: - matrix: - Python37: - python.version: '3.7' - Python38: - python.version: '3.8' - Python39: - python.version: '3.9' +jobs: +- job: 'Tests' + pool: + vmImage: 'ubuntu-16.04' + strategy: + matrix: + Python37: + python.version: '3.7' + Python38: + python.version: '3.8' + Python39: + python.version: '3.9' -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - displayName: 'Use Python $(python.version)' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + displayName: 'Use Python $(python.version)' -- script: | - python -m pip install --upgrade pip wheel - displayName: 'Install build tools' + - script: | + python -m pip install --upgrade pip wheel + displayName: 'Install build tools' -- script: | - pip install . - displayName: 'Install library' + - script: | + pip install . + displayName: 'Install library' -- script: | - pip install pytest pytest-azurepipelines pytest-cov - pytest . - displayName: 'Run tests' + - script: | + pip install pytest pytest-azurepipelines pytest-cov + pytest . + displayName: 'Run tests' + +- job: 'Lint' + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + + - script: | + pip install flake8 + flake8 . + displayName: 'Run linting' From bc102696b8f789df8db11f76fc96c6d184fd0537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Sat, 13 Feb 2021 14:13:06 -0500 Subject: [PATCH 17/26] Remove linting standalone script --- .azure-pipelines/linting.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .azure-pipelines/linting.yml diff --git a/.azure-pipelines/linting.yml b/.azure-pipelines/linting.yml deleted file mode 100644 index b249f5123..000000000 --- a/.azure-pipelines/linting.yml +++ /dev/null @@ -1,13 +0,0 @@ -trigger: -- master -- dev - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: '3.9' - -- script: | - pip install flake8 - flake8 . - displayName: 'Run linting' \ No newline at end of file From 35e358e2df2e67c1a9989c1bf302eae5378d346b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 07:13:18 -0400 Subject: [PATCH 18/26] Changed to Ubuntu latest --- .azure-pipelines/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index ef6cf0a75..9eb813e0e 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -5,7 +5,7 @@ trigger: jobs: - job: 'Tests' pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' strategy: matrix: Python37: @@ -36,7 +36,7 @@ jobs: - job: 'Lint' pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 inputs: From 72723448b3ac21c59fcedec8bd2e9689f4c20e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 10:35:11 -0400 Subject: [PATCH 19/26] Fixed tests and added a bunch of things (that were tested but did not exist) --- deap/base.py | 88 +++++++++++ deap/cma.py | 320 ++++++++++++++++++++++++++++++++++++++ deap/tools/mutation.py | 1 - tests/test_algorithms.py | 44 ++++-- tests/test_convergence.py | 20 +-- tests/test_mutation.py | 2 +- tests/test_operators.py | 5 +- tests/test_pickle.py | 2 + 8 files changed, 450 insertions(+), 32 deletions(-) diff --git a/deap/base.py b/deap/base.py index c88ee4611..e0df3fcb9 100644 --- a/deap/base.py +++ b/deap/base.py @@ -268,3 +268,91 @@ def __repr__(self): """Return the Python code to build a copy of the object.""" return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, self.values if self.valid else tuple()) + + +def _violates_constraint(fitness): + return not fitness.valid \ + and fitness.constraint_violation is not None \ + and sum(fitness.constraint_violation) > 0 + + +class ConstrainedFitness(Fitness): + def __init__(self, values=(), constraint_violation=None): + super(ConstrainedFitness, self).__init__(values) + self.constraint_violation = constraint_violation + + @Fitness.values.deleter + def values(self): + self.wvalues = () + self.constraint_violation = None + + def __gt__(self, other): + return not self.__le__(other) + + def __ge__(self, other): + return not self.__lt__(other) + + def __le__(self, other): + self_violates_constraints = _violates_constraint(self) + other_violates_constraints = _violates_constraint(other) + + if self_violates_constraints and other_violates_constraints: + return True + elif self_violates_constraints: + return True + elif other_violates_constraints: + return False + + return self.wvalues <= other.wvalues + + def __lt__(self, other): + self_violates_constraints = _violates_constraint(self) + other_violates_constraints = _violates_constraint(other) + + if self_violates_constraints and other_violates_constraints: + return False + elif self_violates_constraints: + return True + elif other_violates_constraints: + return False + + return self.wvalues < other.wvalues + + def __eq__(self, other): + self_violates_constraints = _violates_constraint(self) + other_violates_constraints = _violates_constraint(other) + + if self_violates_constraints and other_violates_constraints: + return True + elif self_violates_constraints: + return False + elif other_violates_constraints: + return False + + return self.wvalues == other.wvalues + + def __ne__(self, other): + return not self.__eq__(other) + + def dominates(self, other): + self_violates_constraints = _violates_constraint(self) + other_violates_constraints = _violates_constraint(other) + + if self_violates_constraints and other_violates_constraints: + return False + elif self_violates_constraints: + return False + elif other_violates_constraints: + return True + + return super(ConstrainedFitness, self).dominates(other) + + def __str__(self): + """Return the values of the Fitness object.""" + return str((self.values if self.valid else tuple(), self.constraint_violation)) + + def __repr__(self): + """Return the Python code to build a copy of the object.""" + return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, + self.values if self.valid else tuple(), + self.constraint_violation) \ No newline at end of file diff --git a/deap/cma.py b/deap/cma.py index a886b81f7..9b6a20504 100644 --- a/deap/cma.py +++ b/deap/cma.py @@ -22,6 +22,8 @@ """ import copy from math import sqrt, log, exp +from itertools import cycle + import numpy from . import tools @@ -545,3 +547,321 @@ def update(self, population): self.A = [A[i] if ind._ps[0] == "o" else self.A[ind._ps[1]] for i, ind in enumerate(chosen)] self.pc = [pc[i] if ind._ps[0] == "o" else self.pc[ind._ps[1]] for i, ind in enumerate(chosen)] self.psucc = [psucc[i] if ind._ps[0] == "o" else self.psucc[ind._ps[1]] for i, ind in enumerate(chosen)] + + +class StrategyActiveOnePlusLambda(object): + """A CMA-ES strategy that combines the :math:`(1 + \\lambda)` paradigm + [Igel2007]_, the mixed integer modification [Hansen2011]_, active + covariance update [Arnold2010]_ and constraint handling [Arnold2012]_. + This version of CMA-ES requires the random vector and the mutation + that created each individual. The vector and mutation are stored in each + individual as :attr:`_z` and :attr:`_y` respectively. Updating with + individuals not containing these attributes will result in an + :class:`AttributeError`. + Notes: + When using this strategy (especially when using constraints) you should + monitor the strategy :attr:`condition_number`. If it goes above a given + threshold (say :math:`10^{12}`), you should think of restarting the + optimization as the covariance matrix is going degenerate. See the + constrained active CMA-ES example for a simple example of restart. + :param parent: An iterable object that indicates where to start the + evolution. The parent requires a fitness attribute. + :param sigma: The initial standard deviation of the distribution. + :param step: The minimal step size for each dimension. Use 0 for + continuous dimensions. + :param lambda_: Number of offspring to produce from the parent. + (optional, defaults to 1) + :param **kwargs: One or more parameter to pass to the strategy as + described in the following table. (optional) + +----------------+---------------------------+------------------------------+ + | Parameter | Default | Details | + +================+===========================+==============================+ + | ``d`` | ``1.0 + N / (2.0 * | Damping for step-size. | + | | lambda_)`` | | + +----------------+---------------------------+------------------------------+ + | ``ptarg`` | ``1.0 / (5 + sqrt(lambda_)| Taget success rate | + | | / 2.0)`` | (from 1 + lambda algorithm). | + +----------------+---------------------------+------------------------------+ + | ``cp`` | ``ptarg * lambda_ / (2.0 +| Step size learning rate. | + | | ptarg * lambda_)`` | | + +----------------+---------------------------+------------------------------+ + | ``cc`` | ``2.0 / (N + 2.0)`` | Cumulation time horizon. | + +----------------+---------------------------+------------------------------+ + | ``ccov`` | ``2.0 / (N**2 + 6.0)`` | Covariance matrix learning | + | | | rate. | + +----------------+---------------------------+------------------------------+ + | ``ccovn`` | ``0.4 / (N**1.6 + 1.0)`` | Covariance matrix negative | + | | | learning rate. | + +----------------+---------------------------+------------------------------+ + | ``cconst`` | ``1.0 / (N + 2.0)`` | Constraint vectors learning | + | | | rate. | + +----------------+---------------------------+------------------------------+ + | ``beta`` | ``0.1 / (lambda_ * (N + | Covariance matrix learning | + | | 2.0))`` | rate for constraints. | + | | | | + +----------------+---------------------------+------------------------------+ + | ``pthresh`` | ``0.44`` | Threshold success rate. | + +----------------+---------------------------+------------------------------+ + .. [Igel2007] Igel, Hansen and Roth. Covariance matrix adaptation for + multi-objective optimization. 2007 + .. [Arnold2010] Arnold and Hansen. Active covariance matrix adaptation for + the (1+1)-CMA-ES. 2010. + .. [Hansen2011] Hansen. A CMA-ES for Mixed-Integer Nonlinear Optimization. + Research Report] RR-7751, INRIA. 2011 + .. [Arnold2012] Arnold and Hansen. A (1+1)-CMA-ES for Constrained Optimisation. + 2012 + """ + def __init__(self, parent, sigma, steps, **kargs): + self.parent = parent + self.sigma = sigma + self.dim = len(self.parent) + + self.A = numpy.identity(self.dim) + self.invA = numpy.identity(self.dim) + self.condition_number = numpy.linalg.cond(self.A) + + self.pc = numpy.zeros(self.dim) + + # Save parameters + self.params = kargs.copy() + + # Covariance matrix adaptation + self.cc = self.params.get("cc", 2.0 / (self.dim + 2.0)) + self.ccovp = self.params.get("ccovp", 2.0 / (self.dim ** 2 + 6.0)) + self.ccovn = self.params.get("ccovn", 0.4 / (self.dim ** 1.6 + 1.0)) + self.cconst = self.params.get("cconst", 1.0 / (self.dim + 2.0)) + self.pthresh = self.params.get("pthresh", 0.44) + + self.lambda_ = self.params.get("lambda_", 1) + + self.psucc = self.ptarg + self.S_int = numpy.array(steps) + self.i_I_R = numpy.flatnonzero(2 * self.sigma * numpy.diag(self.A)**0.5 + < self.S_int) + + self.constraint_vecs = None + self.ancestors_fitness = list() + + @property + def lambda_(self): + return self._lambda + + @lambda_.setter + def lambda_(self, value): + self._lambda = value + self._compute_lambda_parameters() + + def _compute_lambda_parameters(self): + """Computes the parameters depending on :math:`\lambda`. It needs to + be called again if :math:`\lambda` changes during evolution. + """ + # Step size control : + self.d = self.params.get("d", 1.0 + self.dim / (2.0 * self.lambda_)) + self.ptarg = self.params.get("ptarg", 1.0 / (5 + numpy.sqrt(self.lambda_) + / 2.0)) + self.cp = self.params.get("cp", (self.ptarg * self.lambda_ + / (2 + self.ptarg * self.lambda_))) + + self.beta = self.params.get("beta", 0.1 / (self.lambda_ * (self.dim + 2.0))) + + def generate(self, ind_init): + """Generate a population of :math:`\lambda` individuals of type + *ind_init* from the current strategy. + :param ind_init: A function object that is able to initialize an + individual from a list. + :returns: A list of individuals. + """ + # Generate individuals + z = numpy.random.standard_normal((self.lambda_, self.dim)) + y = numpy.dot(self.A, z.T).T + x = self.parent + self.sigma * y + self.S_int * self._integer_mutation() + + if any(self.S_int > 0): + # Bring values to the integer steps + round_values = numpy.tile(self.S_int > 0, (self.lambda_, 1)) + steps = numpy.tile(self.S_int, (self.lambda_, 1)) + x[round_values] = steps[round_values] * numpy.around(x[round_values] + / steps[round_values]) + + # The update method requires to remember the y of each individual + population = list(map(ind_init, x)) + for ind, yi, zi in zip(population, y, z): + ind._y = yi + ind._z = zi + + return population + + def _integer_mutation(self): + n_I_R = self.i_I_R.shape[0] + + # Mixed integer CMA-ES is developped for (mu/mu , lambda) + # We have a (1 + lambda) setting, thus we make the integer mutation + # probabilistic. The integer mutation is lambda / 2 if all dimensions + # are integers or min(lambda / 2 - 1, lambda / 10 + n_I_R + 1). The minus + # 1 accounts for the last new candidate getting its integer mutation from + # the last best solution. We skip this last best solution part. + if n_I_R == 0: + return numpy.zeros((self.lambda_, self.dim)) + elif n_I_R == self.dim: + p = self.lambda_ / 2.0 / self.lambda_ + # lambda_int = int(numpy.floor(self.lambda_ / 2)) + else: + p = (min(self.lambda_ / 2.0, self.lambda_ / 10.0 + n_I_R / self.dim) + / self.lambda_) + # lambda_int = int(min(numpy.floor(self.lambda_ / 10) + n_I_R + 1, + # numpy.floor(self.lambda_ / 2) - 1)) + + Rp = numpy.zeros((self.lambda_, self.dim)) + Rpp = numpy.zeros((self.lambda_, self.dim)) + + # Ri' has exactly one of its components set to one. + # The Ri' are dependent in that the number of mutations for each coordinate + # differs at most by one + for i, j in zip(range(self.lambda_), cycle(self.i_I_R)): + # Probabilistically choose lambda_int individuals + if numpy.random.rand() < p: + Rp[i, j] = 1 + Rpp[i, j] = numpy.random.geometric(p=0.7**(1.0/n_I_R)) - 1 + + I_pm1 = (-1)**numpy.random.randint(0, 2, (self.lambda_, self.dim)) + R_int = I_pm1 * (Rp + Rpp) + + # Usually in mu/mu, lambda the last individual is set to the step taken. + # We don't use this sheme in the 1 + lambda scheme + # if self.update_count > 0: + # R_int[-1, :] = (numpy.floor(-self.S_int - self.last_best) + # - numpy.floor(-self.S_int - self.centroid)) + + return R_int + + def _rank1update(self, individual, p_succ): + update_cov = False + self.psucc = (1 - self.cp) * self.psucc + self.cp * p_succ + + if not hasattr(self.parent, "fitness") \ + or self.parent.fitness <= individual.fitness: + self.parent = copy.deepcopy(individual) + self.ancestors_fitness.append(copy.deepcopy(individual.fitness)) + if len(self.ancestors_fitness) > 5: + self.ancestors_fitness.pop() + + # Must guard if pc is all 0 to prevent w_norm_sqrd to be 0 + if self.psucc < self.pthresh or numpy.allclose(self.pc, 0): + self.pc = (1 - self.cc) * self.pc + (numpy.sqrt(self.cc * (2 - self.cc)) + * individual._y) + + a = numpy.sqrt(1 - self.ccovp) + w = numpy.dot(self.invA, self.pc) + w_norm_sqrd = numpy.linalg.norm(w) ** 2 + b = numpy.sqrt(1 - self.ccovp) / w_norm_sqrd \ + * (numpy.sqrt(1 + self.ccovp / (1 - self.ccovp) * w_norm_sqrd) + - 1) + + else: + self.pc = (1 - self.cc) * self.pc + + d = self.ccovp * (1 + self.cc * (2 - self.cc)) + a = numpy.sqrt(1 - d) + w = numpy.dot(self.invA, self.pc) + w_norm_sqrd = numpy.linalg.norm(w) ** 2 + b = numpy.sqrt(1 - d) \ + * (numpy.sqrt(1 + self.ccovp * w_norm_sqrd / (1 - d)) - 1) \ + / w_norm_sqrd + + update_cov = True + + elif len(self.ancestors_fitness) >= 5 \ + and individual.fitness < self.ancestors_fitness[0] \ + and self.psucc < self.pthresh: + # Active covariance update requires w = z and not w = inv(A)s + w = individual._z + w_norm_sqrd = numpy.linalg.norm(w) ** 2 + if 1 < self.ccovn * (2 * w_norm_sqrd - 1): + ccovn = 1 / (2 * w_norm_sqrd - 1) + else: + ccovn = self.ccovn + + a = numpy.sqrt(1 + ccovn) + b = numpy.sqrt(1 + ccovn) / w_norm_sqrd \ + * (numpy.sqrt(1 - ccovn / (1 + ccovn) * w_norm_sqrd) - 1) + update_cov = True + + if update_cov: + self.A = self.A * a + b * numpy.outer(numpy.dot(self.A, w), w) + self.invA = (1 / a * self.invA + - b / (a ** 2 + a * b * w_norm_sqrd) + * numpy.dot(self.invA, numpy.outer(w, w))) + + # TODO: Add integer mutation i_I_R component + self.sigma = self.sigma * numpy.exp(1.0 / self.d + * ((self.psucc - self.ptarg) + / (1.0 - self.ptarg))) + + def _infeasible_update(self, individual): + if not hasattr(individual.fitness, "constraint_violation"): + return + + if self.constraint_vecs is None: + shape = len(individual.fitness.constraint_violation), self.dim + self.constraint_vecs = numpy.zeros(shape) + + for i in range(self.constraint_vecs.shape[0]): + if individual.fitness.constraint_violation[i]: + self.constraint_vecs[i] = (1 - self.cconst) * self.constraint_vecs[i] \ + + self.cconst * individual._y + + W = numpy.dot(self.invA, self.constraint_vecs.T).T # M x N + constraint_violation = numpy.sum(individual.fitness.constraint_violation) + + A_prime = ( + self.A - self.beta / constraint_violation + * numpy.sum( + list( + numpy.outer(self.constraint_vecs[i], W[i]) + / numpy.dot(W[i], W[i]) + for i in range(self.constraint_vecs.shape[0]) + if individual.fitness.constraint_violation[i] + ), + axis=0 + ) + ) + + try: + self.invA = numpy.linalg.inv(A_prime) + except numpy.linalg.LinAlgError: + warnings.warn("Singular matrix inversion, " + "invalid update in CMA-ES ignored", RuntimeWarning) + else: + self.A = A_prime + + def update(self, population): + """Update the current covariance matrix strategy from the *population*. + :param population: A list of individuals from which to update the + parameters. + """ + valid_population = [ind for ind in population if ind.fitness.valid] + invalid_population = [ind for ind in population if not ind.fitness.valid] + + if len(valid_population) > 0: + # Rank 1 update + valid_population.sort(key=lambda ind: ind.fitness, reverse=True) + if not hasattr(self.parent, "fitness"): + lambda_succ = len(valid_population) + else: + lambda_succ = sum(self.parent.fitness <= ind.fitness + for ind in valid_population) + # Use len(valid) to not account for individuals violating constraints + self._rank1update(valid_population[0], + float(lambda_succ) / len(valid_population)) + + if len(invalid_population) > 0 : + # Learn constraint from all invalid individuals + for ind in invalid_population: + self._infeasible_update(ind) + + # Used to monitor the convariance matrix conditioning + self.condition_number = numpy.linalg.cond(self.A) + + C = numpy.dot(self.A, self.A.T) + self.i_I_R = numpy.flatnonzero(2 * self.sigma * numpy.diag(C)**0.5 + < self.S_int) \ No newline at end of file diff --git a/deap/tools/mutation.py b/deap/tools/mutation.py index 1f0fefced..e13a7e429 100644 --- a/deap/tools/mutation.py +++ b/deap/tools/mutation.py @@ -2,7 +2,6 @@ import random from itertools import repeat -from past.builtins import xrange try: from collections.abc import Sequence diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 62e17a757..75cc4f66b 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU Lesser General Public # License along with DEAP. If not, see . -from nose import with_setup import random import numpy +import pytest from deap import algorithms from deap import base @@ -32,25 +32,37 @@ HV_THRESHOLD = 116.0 # 120.777 is Optimal value -def setup_func_single_obj(): +def teardown_(): + # Messy way to remove a class from the creator + del creator.__dict__[FITCLSNAME] + del creator.__dict__[INDCLSNAME] + + +@pytest.fixture +def setup_teardown_single_obj(): creator.create(FITCLSNAME, base.Fitness, weights=(-1.0,)) creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + yield + teardown_() + -def setup_func_multi_obj(): +@pytest.fixture +def setup_teardown_multi_obj(): creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) creator.create(INDCLSNAME, list, fitness=creator.__dict__[FITCLSNAME]) + yield + teardown_() -def setup_func_multi_obj_numpy(): + +@pytest.fixture +def setup_teardown_multi_obj_numpy(): creator.create(FITCLSNAME, base.Fitness, weights=(-1.0, -1.0)) creator.create(INDCLSNAME, numpy.ndarray, fitness=creator.__dict__[FITCLSNAME]) + yield + teardown_() -def teardown_func(): - # Messy way to remove a class from the creator - del creator.__dict__[FITCLSNAME] - del creator.__dict__[INDCLSNAME] -@with_setup(setup_func_single_obj, teardown_func) -def test_cma(): +def test_cma(setup_teardown_single_obj): NDIM = 5 strategy = cma.Strategy(centroid=[0.0]*NDIM, sigma=1.0) @@ -65,8 +77,8 @@ def test_cma(): assert best.fitness.values < (1e-8,), "CMA algorithm did not converged properly." -@with_setup(setup_func_multi_obj, teardown_func) -def test_nsga2(): + +def test_nsga2(setup_teardown_multi_obj): NDIM = 5 BOUND_LOW, BOUND_UP = 0.0, 1.0 MU = 16 @@ -116,8 +128,7 @@ def test_nsga2(): assert not (any(numpy.asarray(ind) < BOUND_LOW) or any(numpy.asarray(ind) > BOUND_UP)) -@with_setup(setup_func_multi_obj_numpy, teardown_func) -def test_mo_cma_es(): +def test_mo_cma_es(setup_teardown_multi_obj_numpy): def distance(feasible_ind, original_ind): """A distance function to the feasibility region.""" @@ -186,8 +197,7 @@ def valid(individual): assert hv > HV_THRESHOLD, "Hypervolume is lower than expected %f < %f" % (hv, HV_THRESHOLD) -@with_setup(setup_func_multi_obj, teardown_func) -def test_nsga3(): +def test_nsga3(setup_teardown_multi_obj): NDIM = 5 BOUND_LOW, BOUND_UP = 0.0, 1.0 MU = 16 @@ -211,7 +221,7 @@ def test_nsga3(): ind.fitness.values = fit pop = toolbox.select(pop, len(pop)) - # Begin the generational process + # Begin the generational process for gen in range(1, NGEN): offspring = algorithms.varAnd(pop, toolbox, 1.0, 1.0) diff --git a/tests/test_convergence.py b/tests/test_convergence.py index 1d1856d58..1309392d6 100644 --- a/tests/test_convergence.py +++ b/tests/test_convergence.py @@ -42,6 +42,7 @@ def tearDown(self): del creator.__dict__[FITCLSNAME] del creator.__dict__[INDCLSNAME] + class TestSingleObjective(TearDownCreatorTestCase): def setUp(self): creator.create(FITCLSNAME, base.Fitness, weights=(-1.0,)) @@ -51,7 +52,7 @@ def test_cma(self): NDIM = 5 NGEN = 100 - strategy = cma.BasicStrategy(centroid=[0.0]*NDIM, sigma=1.0) + strategy = cma.Strategy(centroid=[0.0]*NDIM, sigma=1.0) toolbox = base.Toolbox() toolbox.register("evaluate", benchmarks.sphere) @@ -59,8 +60,9 @@ def test_cma(self): toolbox.register("update", strategy.update) # Consume the algorithm until NGEN - state = next(islice(algorithms.GenerateUpdateAlgorithm(toolbox), NGEN, None)) - best, = tools.selBest(state.population, k=1) + population, _ = algorithms.eaGenerateUpdate(toolbox, NGEN) + + best, = tools.selBest(population, k=1) self.assertLess(best.fitness.values[0], 1e-8) @@ -73,7 +75,7 @@ def test_cma_mixed_integer_1_p_1_no_constraint(self): parent = (numpy.random.rand(N) * 2) + 1 - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=1) + strategy = cma.StrategyActiveOnePlusLambda(parent, 0.5, [0, 0, 0.1], lambda_=1) toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) toolbox.register("update", strategy.update) @@ -109,7 +111,7 @@ def test_cma_mixed_integer_1_p_20_no_constraint(self): parent = (numpy.random.rand(N) * 2) + 1 - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1], lambda_=20) + strategy = cma.StrategyActiveOnePlusLambda(parent, 0.5, [0, 0, 0.1], lambda_=20) toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) toolbox.register("update", strategy.update) @@ -166,7 +168,7 @@ def c2(individual): while restarts > 0: parent = (numpy.random.rand(N) * 2) + 1 - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=1) + strategy = cma.StrategyActiveOnePlusLambda(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=1) toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) toolbox.register("update", strategy.update) @@ -226,7 +228,7 @@ def c2(individual): while restarts > 0: parent = (numpy.random.rand(N) * 2) + 1 - strategy = cma.ActiveOnePlusLambdaStrategy(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=20) + strategy = cma.StrategyActiveOnePlusLambda(parent, 0.5, [0, 0, 0.1, 0, 0], lambda_=20) toolbox.register("generate", strategy.generate, ind_init=creator.__dict__[INDCLSNAME]) toolbox.register("update", strategy.update) @@ -345,7 +347,7 @@ def test_nsga3(self): # Begin the generational process for gen in range(1, NGEN): # Vary the individuals - offspring = list(islice(algorithms.and_variation(pop, toolbox, 1.0, 1.0), len(pop))) + offspring = list(islice(algorithms.varAnd(pop, toolbox, 1.0, 1.0), len(pop))) # Evaluate the individuals with an invalid fitness invalid_ind = [ind for ind in offspring if not ind.fitness.valid] @@ -407,7 +409,7 @@ def valid(individual): for ind in population: ind.fitness.values = toolbox.evaluate(ind) - strategy = cma.MultiObjectiveStrategy(population, sigma=1.0, mu=MU, lambda_=LAMBDA) + strategy = cma.StrategyMultiObjective(population, sigma=1.0, mu=MU, lambda_=LAMBDA) toolbox.register("generate", strategy.generate, creator.__dict__[INDCLSNAME]) toolbox.register("update", strategy.update) diff --git a/tests/test_mutation.py b/tests/test_mutation.py index 2822f8a83..65c7742e1 100644 --- a/tests/test_mutation.py +++ b/tests/test_mutation.py @@ -1,5 +1,5 @@ import unittest -import mock +from unittest import mock from deap.tools.mutation import mutInversion diff --git a/tests/test_operators.py b/tests/test_operators.py index d4d0ba24e..27b10eab9 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -1,8 +1,5 @@ import unittest -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock import random from deap.tools import crossover diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 873db8f46..b8e54054d 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -13,9 +13,11 @@ from deap import gp from deap import tools + def func(): return "True" + class Pickling(unittest.TestCase): def setUp(self): From de015af6edd601e636ade1ca9d9b1bc58e5064ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 20:38:18 -0400 Subject: [PATCH 20/26] Bumped Python test versions --- .azure-pipelines/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 9eb813e0e..cc8064b46 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -8,12 +8,12 @@ jobs: vmImage: 'ubuntu-latest' strategy: matrix: - Python37: - python.version: '3.7' - Python38: - python.version: '3.8' Python39: python.version: '3.9' + Python310: + python.version: '3.10' + Python311: + python.version: '3.11' steps: - task: UsePythonVersion@0 From f04bfcbd25cf5abd9be2d9bd3970538573bec63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 20:40:36 -0400 Subject: [PATCH 21/26] Removed Lint checks for now --- .azure-pipelines/ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index cc8064b46..c0bbcf0aa 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -34,15 +34,15 @@ jobs: pytest . displayName: 'Run tests' -- job: 'Lint' - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.9' +# - job: 'Lint' +# pool: +# vmImage: 'ubuntu-latest' +# steps: +# - task: UsePythonVersion@0 +# inputs: +# versionSpec: '3.9' - - script: | - pip install flake8 - flake8 . - displayName: 'Run linting' +# - script: | +# pip install flake8 +# flake8 . +# displayName: 'Run linting' From b119a42478790e953ee2fff1791dc3c8846e9d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 20:46:04 -0400 Subject: [PATCH 22/26] Changed copy_reg to copyreg --- deap/creator.py | 1 - deap/gp.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deap/creator.py b/deap/creator.py index 37d2eca53..16b0b76df 100644 --- a/deap/creator.py +++ b/deap/creator.py @@ -24,7 +24,6 @@ import array import copy import warnings -import copy_reg class_replacers = {} """Some classes in Python's standard library as well as third party library diff --git a/deap/gp.py b/deap/gp.py index 2606a310e..e662c9f77 100644 --- a/deap/gp.py +++ b/deap/gp.py @@ -21,7 +21,7 @@ """ import copy import math -import copy_reg +import copyreg import random import re import sys @@ -275,7 +275,7 @@ def __init__(cls, name, func, ret=__type__, id_=None): def __reduce__(cls): return (MetaEphemeral, (cls.name, cls.func, cls.ret, id(cls))) -copy_reg.pickle(MetaEphemeral, MetaEphemeral.__reduce__) +copyreg.pickle(MetaEphemeral, MetaEphemeral.__reduce__) class PrimitiveSetTyped(object): From e2ffc05d4435dd1334d1d8ac871d47f58dca16b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Tue, 18 Jul 2023 20:55:40 -0400 Subject: [PATCH 23/26] Reintroduce lost metacreator and bumped to py3 --- deap/creator.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/deap/creator.py b/deap/creator.py index 16b0b76df..2d8dc520e 100644 --- a/deap/creator.py +++ b/deap/creator.py @@ -23,6 +23,7 @@ import array import copy +import copyreg import warnings class_replacers = {} @@ -90,8 +91,55 @@ def __deepcopy__(self, memo): def __reduce__(self): return (self.__class__, (list(self),), self.__dict__) + + class_replacers[array.array] = _array + +class MetaCreator(type): + def __new__(cls, name, base, dct): + return super(MetaCreator, cls).__new__(cls, name, (base,), dct) + + def __init__(cls, name, base, dct): + # A DeprecationWarning is raised when the object inherits from the + # class "object" which leave the option of passing arguments, but + # raise a warning stating that it will eventually stop permitting + # this option. Usually this happens when the base class does not + # override the __init__ method from object. + dict_inst = {} + dict_cls = {} + for obj_name, obj in dct.items(): + if isinstance(obj, type): + dict_inst[obj_name] = obj + else: + dict_cls[obj_name] = obj + + def init_type(self, *args, **kargs): + """Replace the __init__ function of the new type, in order to + add attributes that were defined with **kargs to the instance. + """ + for obj_name, obj in dict_inst.items(): + setattr(self, obj_name, obj()) + if base.__init__ is not object.__init__: + base.__init__(self, *args, **kargs) + + cls.__init__ = init_type + cls.reduce_args = (name, base, dct) + super(MetaCreator, cls).__init__(name, (base,), dict_cls) + + def __reduce__(cls): + return (meta_create, cls.reduce_args) + + +copyreg.pickle(MetaCreator, MetaCreator.__reduce__) + + +def meta_create(name, base, dct): + class_ = MetaCreator(name, base, dct) + globals()[name] = class_ + return class_ + + def create(name, base, **kargs): """Creates a new class named *name* inheriting from *base* in the :mod:`~deap.creator` module. The new class can have attributes defined by From 77c7e4cd6f1b6670f11056a4c0d532cac34b7e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Wed, 19 Jul 2023 09:05:42 -0400 Subject: [PATCH 24/26] Removed fused old creator tests --- deap/tests/test_creator.py | 89 -------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 deap/tests/test_creator.py diff --git a/deap/tests/test_creator.py b/deap/tests/test_creator.py deleted file mode 100644 index a65f45334..000000000 --- a/deap/tests/test_creator.py +++ /dev/null @@ -1,89 +0,0 @@ -# This file is part of DEAP. -# -# DEAP is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# DEAP is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with DEAP. If not, see . - -from nose import with_setup -from unittest import skipIf - -import array - -try: - import numpy -except ImportError: - numpy = False - -from deap import creator - -CNAME = "CLASS_NAME" - - -def teardown_func(): - creator.__dict__.pop(CNAME) - - -@with_setup(None, teardown_func) -def test_create(): - creator.create(CNAME, list) - l = creator.__dict__[CNAME]([1, 2, 3, 4]) - - assert l == [1, 2, 3, 4], "%s, expected %s" % (l, [1, 2, 3, 4]) - - -@with_setup(None, teardown_func) -def test_attribute(): - creator.create(CNAME, list, a=1) - l = creator.__dict__[CNAME]([1, 2, 3, 4]) - - assert l.a == 1, "%s, expected %i" % (l.a, 1) - - -@with_setup(None, teardown_func) -def test_array(): - creator.create(CNAME, array.array, typecode="i") - a = creator.__dict__[CNAME]([1, 2, 3, 4]) - b = creator.__dict__[CNAME]([5, 6, 7, 8]) - - a[1:3], b[1:3] = b[1:3], a[1:3] - ta = array.array("i", [1, 6, 7, 4]) - tb = array.array("i", [5, 2, 3, 8]) - assert a == ta, "%s, expected %s" % (a, ta) - assert b == tb, "%s, expected %s" % (b, tb) - - -@skipIf(not numpy, "Cannot import Numpy numerical library") -@with_setup(None, teardown_func) -def test_numpy_nocopy(): - creator.create(CNAME, numpy.ndarray) - a = creator.__dict__[CNAME]([1, 2, 3, 4]) - b = creator.__dict__[CNAME]([5, 6, 7, 8]) - - a[1:3], b[1:3] = b[1:3], a[1:3] - ta = numpy.array([1, 6, 7, 4]) - tb = numpy.array([5, 6, 7, 8]) - assert all(a == ta), "%s, expected %s" % (a, ta) - assert all(b == tb), "%s, expected %s" % (b, tb) - - -@skipIf(not numpy, "Cannot import Numpy numerical library") -@with_setup(None, teardown_func) -def test_numpy_copy(): - creator.create(CNAME, numpy.ndarray) - a = creator.__dict__[CNAME]([1, 2, 3, 4]) - b = creator.__dict__[CNAME]([5, 6, 7, 8]) - - a[1:3], b[1:3] = b[1:3].copy(), a[1:3].copy() - ta = numpy.array([1, 6, 7, 4]) - tb = numpy.array([5, 2, 3, 8]) - assert all(a == ta), "%s, expected %s" % (a, ta) - assert all(b == tb), "%s, expected %s" % (b, tb) From fc28a1b84a4ec8808642a90f8f3c0972e649d13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Wed, 19 Jul 2023 09:07:51 -0400 Subject: [PATCH 25/26] Import warnings --- deap/cma.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deap/cma.py b/deap/cma.py index 9b6a20504..e458f47b5 100644 --- a/deap/cma.py +++ b/deap/cma.py @@ -23,6 +23,7 @@ import copy from math import sqrt, log, exp from itertools import cycle +import warnings import numpy From bfccb179f53ce4006f1fb12d30f015f2d9a5a9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Michel=20De=20Rainville?= Date: Wed, 19 Jul 2023 15:38:41 -0400 Subject: [PATCH 26/26] Added deleted class unpickling --- tests/test_pickle.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/test_pickle.py b/tests/test_pickle.py index 8baa2073e..0d7bb9d2d 100644 --- a/tests/test_pickle.py +++ b/tests/test_pickle.py @@ -70,6 +70,16 @@ def test_pickle_ind_ndarray(self): self.assertTrue(all(ind == ind_l), "Unpickled individual numpy.ndarray != pickled individual numpy.ndarray") self.assertEqual(ind.fitness, ind_l.fitness, "Unpickled individual fitness != pickled individual fitness") + def test_pickle_delete_ind_list(self): + creator.create("TempInd", list, fitness=creator.FitnessMax) + ind = creator.TempInd([1.0, 2.0, 3.0]) + del creator.TempInd + ind.fitness.values = (4.0,) + ind_s = pickle.dumps(ind) + ind_l = pickle.loads(ind_s) + self.assertEqual(ind, ind_l, "Unpickled individual list != pickled individual list") + self.assertEqual(ind.fitness, ind_l.fitness, "Unpickled individual fitness != pickled individual fitness") + def test_pickle_tree_input(self): pset = gp.PrimitiveSetTyped("MAIN", [int], int, "IN") pset.addPrimitive(operator.add, [int, int], int) @@ -79,9 +89,9 @@ def test_pickle_tree_input(self): ind.fitness.values = (1.0,) ind_s = pickle.dumps(ind, pickle.HIGHEST_PROTOCOL) ind_l = pickle.loads(ind_s) - msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) + msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) self.assertEqual(ind, ind_l, msg) - msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) + msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) self.assertEqual(ind.fitness, ind_l.fitness, msg) def test_pickle_tree_term(self): @@ -94,9 +104,9 @@ def test_pickle_tree_term(self): ind.fitness.values = (1.0,) ind_s = pickle.dumps(ind, pickle.HIGHEST_PROTOCOL) ind_l = pickle.loads(ind_s) - msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) + msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) self.assertEqual(ind, ind_l, msg) - msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) + msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) self.assertEqual(ind.fitness, ind_l.fitness, msg) def test_pickle_tree_ephemeral(self): @@ -109,17 +119,17 @@ def test_pickle_tree_ephemeral(self): ind.fitness.values = (1.0,) ind_s = pickle.dumps(ind, pickle.HIGHEST_PROTOCOL) ind_l = pickle.loads(ind_s) - msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) + msg = "Unpickled individual %s != pickled individual %s" % (str(ind), str(ind_l)) self.assertEqual(ind, ind_l, msg) - msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) + msg = "Unpickled fitness %s != pickled fitness %s" % (str(ind.fitness), str(ind_l.fitness)) self.assertEqual(ind.fitness, ind_l.fitness, msg) def test_pickle_population(self): - ind1 = creator.IndList([1.0,2.0,3.0]) + ind1 = creator.IndList([1.0, 2.0, 3.0]) ind1.fitness.values = (1.0,) - ind2 = creator.IndList([4.0,5.0,6.0]) + ind2 = creator.IndList([4.0, 5.0, 6.0]) ind2.fitness.values = (2.0,) - ind3 = creator.IndList([7.0,8.0,9.0]) + ind3 = creator.IndList([7.0, 8.0, 9.0]) ind3.fitness.values = (3.0,) pop = [ind1, ind2, ind3] @@ -141,7 +151,7 @@ def test_pickle_logbook(self): logbook = tools.Logbook() stats.register("mean", numpy.mean) - record = stats.compile([1,2,3,4,5,6,8,9,10]) + record = stats.compile([1, 2, 3, 4, 5, 6, 8, 9, 10]) logbook.record(**record) logbook_s = pickle.dumps(logbook)