From 434e6bc0eb91e19735584bd4de6d92569a5cd3f2 Mon Sep 17 00:00:00 2001 From: Markus Quade Date: Tue, 11 Sep 2018 13:40:31 +0200 Subject: [PATCH] Mq/pretesting (#57) Add pretesting for glyph-remote --- .gitignore | 1 + README.md | 2 +- doc/source/index.rst | 1 - doc/source/usr/glyph_remote.rst | 90 +++++++++++++ doc/source/usr/gui.rst | 46 ------- examples/remote/experiment.py | 3 + glyph/application.py | 121 ++++++++++++++--- glyph/assessment.py | 20 ++- glyph/cli/_parser.py | 26 +--- glyph/cli/glyph_remote.py | 57 ++++---- glyph/gp/__init__.py | 2 +- glyph/gp/constraints.py | 134 +++++++++++++++---- glyph/observer.py | 22 ++- glyph/utils/__init__.py | 9 +- glyph/utils/break_condition.py | 55 +------- glyph/utils/numeric.py | 12 +- requirements-dev.txt | 3 +- requirements.txt | 1 + setup.py | 2 +- tests/unittest/gp/test_constraints.py | 23 ++-- tests/unittest/test_assessment.py | 18 +++ tests/unittest/utils/test_break_condition.py | 31 ----- 22 files changed, 427 insertions(+), 252 deletions(-) delete mode 100644 doc/source/usr/gui.rst diff --git a/.gitignore b/.gitignore index 8164ad9..22d2162 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +src/ *.sqlite dask-worker-space doc/source/dev/api diff --git a/README.md b/README.md index 2b42ba8..e9b4fe2 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ to their optimization tasks. Installation ------------ -Glyph is a **python 3.5+** only package. +Glyph is a **python 3.6+** only package. You can install the latest stable version from PyPI with pip diff --git a/doc/source/index.rst b/doc/source/index.rst index b8b3327..03d924d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -30,7 +30,6 @@ Content usr/getting_started.rst usr/concepts.rst usr/glyph_remote.rst - usr/gui.rst Publications About diff --git a/doc/source/usr/glyph_remote.rst b/doc/source/usr/glyph_remote.rst index 1152de9..8f7b2fd 100644 --- a/doc/source/usr/glyph_remote.rst +++ b/doc/source/usr/glyph_remote.rst @@ -50,6 +50,8 @@ The possible action values are: +-------------------+--------------------+----------------------------+ | *EXPERIMENT* | list of expressions| list of fitness value(s) | +-------------------+--------------------+----------------------------+ +| *METADATA* | – | any | ++-------------------+--------------------+----------------------------+ | *SHUTDOWN* | – | – | +-------------------+--------------------+----------------------------+ @@ -123,3 +125,91 @@ to one are variables and -1 is reserved for symbolic constants. "x": 0, }, } + + +GUI +--- + + +Install +~~~~~~~ + +Glyph comes with an optional GUI to use the ``glyph-remote`` script with more convenience. + +The GUI uses the package ``wxPython``. The installation manual can be found `here `_ +and `Website `_. + + +**Manual Gooey installtion** + + +Since up-to-date (28.08.2018) the necessary changes to the used graphic library Gooey are not part of the master branch, +it might be necessary to install Gooey by hand from the repo `https://github.com/Magnati/Gooey `_ in three steps. + +- ``pip install -e "git+git@github.com:Magnati/Gooey.git#egg=gooey"`` + + +**Installation with pip installtion** + + +To install glyph including the gui option use the following command: + +.. code-block:: + + python pip install pyglyph[gui]`` + +To start the script with the gui just use the ``--gui`` parameter: + +.. code-block:: + + glyph-remote --gui + +Usage +~~~~~~ + +Within the GUI there is a tab for each group of parameters. +If all parameters are set, click the start-button to start the experiment. + + +Pretesting & Constraints +------------------------ + +In glyph-remote, genetic operations can be constrained. A genetic operation (i.e. every operation that create or modifies the genotype of an individual). +If a constraint is violated, the genetic operation is rejected. If out of time, the last candidate is used. + +Currently, two different types of constraints are implemented: +- algebraic constraints using sympy +- pretesting constraints + +Algebraic constraints +~~~~~~~~~~~~~~~~~~~~~ + +Sympy is used to check whether expressions are: + +- zero +- constant +- infinite + +The three options can be individually activated. + + +Pretesting +~~~~~~~~~~ + +You can invoke file-based pretesting with the `--constraints_pretest filename.py` flag. +The flag `--constraints_pretest_function` lets you pass the function name which will be invoked to pretest individuals. + +The function is expected to return a boolean, depending on the individual is rejected (False) or accepted (True). + +An example file could look like this: + +.. code-block:: + + import time + + + def chi(ind): + time.sleep(1) + print(f"Hello World, this is {ind}") + return True + diff --git a/doc/source/usr/gui.rst b/doc/source/usr/gui.rst deleted file mode 100644 index 0241e4a..0000000 --- a/doc/source/usr/gui.rst +++ /dev/null @@ -1,46 +0,0 @@ - -Glyph remote - GUI -================== - - -Install -''''''' - -Glyph comes with an optional GUI to use the ``glyph-remote`` script with more convenience. - -The GUI uses the package ``wxPython``. The installation manual can be found `here `_ -and `Website `_. - - -Manual Gooey installtion -^^^^^^^^^^^^^^^^^^^^^^^^ - -Since up-to-date (28.08.2018) the necessary changes to the used graphic library Gooey are not part of the master branch, -it might be necessary to install Gooey by hand from the repo `https://github.com/Magnati/Gooey `_ in three steps. - -- ``pip install -e "git+git@github.com:Magnati/Gooey.git#egg=gooey"`` - - -Installation with pip installtion -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To install glyph including the gui option use the following command: - -.. code-block:: - python pip install pyglyph[gui]`` - -To start the script with the gui just use the ``--gui`` parameter: - -.. code-block:: - glyph-remote --gui - -Usage -'''''' - -Within the GUI there is a tab for each group of parameters. -If all parameters are set, click the start-button to start the experiment. - - - - - diff --git a/examples/remote/experiment.py b/examples/remote/experiment.py index 6589fe3..fc35152 100644 --- a/examples/remote/experiment.py +++ b/examples/remote/experiment.py @@ -49,6 +49,9 @@ def work(self, request): action = request['action'] if action == "CONFIG": return self.config + elif action == "PRETEST": + logger.info("Pretest OK") + return dict(ok=True) elif action == "SHUTDOWN": return self.shutdown() elif action == "EXPERIMENT": diff --git a/glyph/application.py b/glyph/application.py index ad1575b..f6b127e 100644 --- a/glyph/application.py +++ b/glyph/application.py @@ -3,6 +3,7 @@ """Convenience classes and functions that allow you to quickly build gp apps.""" +import abc import os import sys import time @@ -34,24 +35,24 @@ def update_logbook_record(runner): if not runner.mstats: runner.mstats = create_stats(len(runner.population[0].fitness.values)) record = runner.mstats.compile(runner.population) - runner.logbook.record(gen=runner.step_count, evals=runner._evals, **record) + runner.logbook.record(gen=runner.step_count, + evals=runner._evals, + **record + ) DEFAULT_CALLBACKS_GP_RUNNER = (update_pareto_front, update_logbook_record) class GPRunner(object): - """Runner for gp problem sets. - - Takes care of propper initialization, execution, and accounting of a gp run - (i.e. population creation, random state, generation count, hall of fame, and - logbook). The method init() has to be called once before stepping through - the evolution process with method step(); init() and step() invoke the - assessment runner. - """ - def __init__(self, IndividualClass, algorithm_factory, assessment_runner, callbacks=DEFAULT_CALLBACKS_GP_RUNNER): - """Init GPRunner. + """Runner for gp problem sets. + + Takes care of propper initialization, execution, and accounting of a gp run + (i.e. population creation, random state, generation count, hall of fame, and + logbook). The method init() has to be called once before stepping through + the evolution process with method step(); init() and step() invoke the + assessment runner. :param IndividualClass: Class inherited from gp.AExpressionTree. :param algorithm_factory: callable() -> gp algorithm, as defined in @@ -137,18 +138,16 @@ def log(app): class Application(object): - """An application based on `GPRunner`. + def __init__(self, config, gp_runner, checkpoint_file=None, callbacks=DEFAULT_CALLBACKS): + """An application based on `GPRunner`. - Controls execution of the runner and adds checkpointing and logging - functionality; also defines a set of available command line options and - their default values. + Controls execution of the runner and adds checkpointing and logging + functionality; also defines a set of available command line options and + their default values. - To create a full console application one can use the factory function - create_console_app(). - """ + To create a full console application one can use the factory function + default_console_app(). - def __init__(self, config, gp_runner, checkpoint_file=None, callbacks=DEFAULT_CALLBACKS): - """ :param config: Container holding all configs :type config: dict or argparse.Namespace :param gp_runner: Instance of `GPRunner` @@ -196,7 +195,12 @@ def run(self, break_condition=None): def _update(self): for cb in self.callbacks: - cb(self) + try: + logger.debug(f"Running callback {cb}.") + cb(self) + except Exception as e: + logger.error(f"Error during execution of {cb}") + logger.warning(e) def checkpoint(self): """Checkpoint current state of evolution.""" @@ -290,6 +294,7 @@ def create(cls, config, *args, **kwargs): return cls._create(config, *args, **kwargs) @staticmethod + @abc.abstractmethod def add_options(parser): """Add available parser options.""" raise NotImplementedError @@ -299,7 +304,7 @@ def get_from_mapping(cls, key): try: func = cls._mapping[key] except KeyError: - raise RuntimeError('Option {} not supported'.format(key)) + raise RuntimeError(f"Option {key} not supported") return func @@ -452,6 +457,78 @@ def add_options(parser): # todo +class ConstraintsFactory(AFactory): + @staticmethod + def add_options(parser): + parser.add_argument( + "--constraints_timeout", + type=utils.argparse.non_negative_int, + default=60, + help="Seconds before giving up and using a new random individual (default: 60)" + ) + parser.add_argument( + "--constraints_n_retries", + type=utils.argparse.non_negative_int, + default=30, + help="Number of genetic operation before giving up and using a new random individual (default: 30)" + ) + parser.add_argument( + "--constraints_zero", + action="store_false", + default=True, + help="Discard zero individuals (default: True)", + ) + parser.add_argument( + "--constraints_constant", + action="store_false", + default=True, + help="Discard constant individuals (default: True)", + ) + parser.add_argument( + "--constraints_infty", + action="store_false", + default=True, + help="Discard individuals with infinities (default: True)", + ) + parser.add_argument( + "--constraints_pretest", + default=False, + help="Path to pretest file." + ) + parser.add_argument( + "--constraints_pretest_function", + type=str, + default="chi", + help="Path to pretest file." + ) + parser.add_argument( + "--constraints_pretest_service", + action="store_true", + help="Use service for pretesting." + ) + + @staticmethod + def _create(config, com=None): + constraints = [] + if config.constraints_zero or config.constraints_infty or config.constraints_constant: + constraints.append( + gp.NonFiniteExpression( + zero=config.constraints_zero, + infty=config.constraints_infty, + constant=config.constraints_constant, + ) + ) + if config.constraints_pretest: + constraints.append( + gp.PreTest(config.constraints_pretest, + fun=config.constraints_pretest_function + ) + ) + # if config.constraints_pretest_service: # todo (enable after com refactor) + # constraints.append(gp.PreTestService(com)) + return gp.Constraint(constraints) + + def safe(file_name, **kwargs): """Dump kwargs to file.""" with open(file_name, "wb") as file: diff --git a/glyph/assessment.py b/glyph/assessment.py index e068fb6..a88ce94 100644 --- a/glyph/assessment.py +++ b/glyph/assessment.py @@ -10,11 +10,14 @@ import warnings import logging +import stopit + from glyph.gp.individual import _get_index logger = logging.getLogger(__name__) + class SingleProcessFactory: map = map @@ -161,7 +164,7 @@ def closure(consts): popt = res.x if res.x.shape else np.array([res.x]) measure_opt = res.fun if not res.success: - logger.debug(res.message) + logger.debug(f"Evaluating {str(individual)}: {res.message}") except ValueError: return p0, closure(p0) if measure_opt is None: @@ -234,3 +237,18 @@ def closure(*args, **kwargs): return res, annotate(closure, {'return': tuple}) return closure + + +def max_fitness_on_timeout(max_fitness, timeout): + """Decorate a function. Associate max_fitness with long running individuals. + + :param max_fitness: fitness of aborted individual calls. + :param timeout: time until timeout + :returns: fitness or max_fitness + """ + def decorate(f): + @functools.wraps(f) + def inner(*args, **kwargs): + return stopit.threading_timeoutable(default=max_fitness)(f)(*args, timeout=timeout, **kwargs) + return inner + return decorate diff --git a/glyph/cli/_parser.py b/glyph/cli/_parser.py index 207a9bf..930fd8d 100644 --- a/glyph/cli/_parser.py +++ b/glyph/cli/_parser.py @@ -143,9 +143,9 @@ def get_parser(parser=None): "-v", "--verbose", dest="verbosity", - action="count", - default=0, - help="set verbose output; raise verbosity level with -vv, -vvv, -vvvv from lv 1-3", + choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"], + default="INFO", + help="Set logging level", ) parser.add_argument( "--logging", @@ -351,24 +351,8 @@ def get_parser(parser=None): ) constraints = parser.add_argument_group("constraints") - constraints.add_argument( - "--constraints_zero", - action="store_false", - default=True, - help="Discard zero individuals (default: True)", - ) - constraints.add_argument( - "--constraints_constant", - action="store_false", - default=True, - help="Discard constant individuals (default: True)", - ) - constraints.add_argument( - "--constraints_infty", - action="store_false", - default=True, - help="Discard individuals with infinities (default: True)", - ) + glyph.application.ConstraintsFactory.add_options(constraints) + observer = parser.add_argument_group("observer") observer.add_argument( diff --git a/glyph/cli/glyph_remote.py b/glyph/cli/glyph_remote.py index d7061f3..c963aca 100644 --- a/glyph/cli/glyph_remote.py +++ b/glyph/cli/glyph_remote.py @@ -30,9 +30,10 @@ from scipy.optimize._minimize import _minimize_neldermead as nelder_mead +from glyph._version import get_versions from glyph.assessment import const_opt from glyph.cli._parser import * # noqa -from glyph.gp.constraints import NullSpace, apply_constraints, build_constraints +from glyph.gp.constraints import constrain from glyph.gp.individual import _constant_normal_form, add_sc, pretty_print, sc_mmqout, simplify_this from glyph.observer import ProgressObserver from glyph.utils import partition, key_set @@ -41,15 +42,16 @@ from glyph.utils.logging import print_params, load_config logger = logging.getLogger(__name__) +version = get_versions()["version"] class ExperimentProtocol(enum.EnumMeta): """Communication Protocol with remote experiments.""" + CONFIG = "CONFIG" EXPERIMENT = "EXPERIMENT" - SHUTDOWN = "SHUTDOWN" METADATA = "METADATA" - CONFIG = "CONFIG" + SHUTDOWN = "SHUTDOWN" class Communicator: @@ -87,6 +89,7 @@ def run(self, break_condition=None): try: super().run(break_condition=break_condition) except KeyboardInterrupt: + logger.info("Received KeyboardInterrupt. Trying checkpointing.") self.checkpoint() finally: self.assessment_runner.com.send(dict(action=ExperimentProtocol.SHUTDOWN)) @@ -121,7 +124,6 @@ def from_checkpoint(cls, file_name, com): def checkpoint(self): """Checkpoint current state of evolution.""" - runner = copy.deepcopy(self.gp_runner) del runner.assessment_runner glyph.application.safe( @@ -132,7 +134,6 @@ def checkpoint(self): pareto_fronts=self.pareto_fronts, callbacks=self.callbacks, ) - logger.debug("Saved checkpoint to {}".format(self.checkpoint_file)) def handle_const_opt_config(args): @@ -448,9 +449,10 @@ def make_remote_app(callbacks=(), callback_factories=(), parser=None): if not os.path.exists(workdir): raise RuntimeError('Path does not exist: "{}"'.format(workdir)) - log_level = glyph.utils.logging.log_level(args.verbosity) glyph.utils.logging.load_config( - config_file=args.logging_config, level=log_level, placeholders=dict(workdir=workdir) + config_file=args.logging_config, + level=getattr(logging, args.verbosity), + placeholders=dict(workdir=workdir) ) if args.resume_file is not None: logger.debug("Loading checkpoint {}".format(args.resume_file)) @@ -461,17 +463,32 @@ def make_remote_app(callbacks=(), callback_factories=(), parser=None): pset = build_pset_gp(args.primitives, args.structural_constants, args.sc_min, args.sc_max) except AttributeError: raise AttributeError("You need to specify the pset") + + assessment_runner = RemoteAssessmentRunner( + com, + method=args.const_opt_method, + options=args.options, + consider_complexity=args.consider_complexity, + caching=args.caching, + persistent_caching=args.persistent_caching, + simplify=args.simplify, + chunk_size=args.chunk_size, + multi_objective=args.multi_objective, + send_symbolic=args.send_symbolic, + reevaluate=args.re_evaluate, + ) + Individual.pset = pset mate = glyph.application.MateFactory.create(args, Individual) mutate = glyph.application.MutateFactory.create(args, Individual) select = glyph.application.SelectFactory.create(args) create_method = glyph.application.CreateFactory.create(args, Individual) - ns = NullSpace( - zero=args.constraints_zero, constant=args.constraints_constant, infty=args.constraints_infty - ) - mate, mutate, Individual.create = apply_constraints( - [mate, mutate, Individual.create], constraints=build_constraints(ns) + mate, mutate, Individual.create = constrain( + [mate, mutate, Individual.create], + glyph.application.ConstraintsFactory.create(args), + n_trials=args.constraints_n_retries, + timeout=args.constraints_timeout, ) ndmate = partial(glyph.gp.breeding.nd_crossover, cx1d=mate) @@ -481,19 +498,7 @@ def make_remote_app(callbacks=(), callback_factories=(), parser=None): algorithm_factory = partial( glyph.application.AlgorithmFactory.create, args, ndmate, ndmutate, select, ndcreate ) - assessment_runner = RemoteAssessmentRunner( - com, - method=args.const_opt_method, - options=args.options, - consider_complexity=args.consider_complexity, - caching=args.caching, - persistent_caching=args.persistent_caching, - simplify=args.simplify, - chunk_size=args.chunk_size, - multi_objective=args.multi_objective, - send_symbolic=args.send_symbolic, - reevaluate=args.re_evaluate, - ) + gp_runner = glyph.application.GPRunner(NDTree, algorithm_factory, assessment_runner) callbacks = glyph.application.DEFAULT_CALLBACKS + callbacks + make_callback(callback_factories, args) @@ -522,7 +527,7 @@ def send_meta_data(app): def main(): app, bc, args = make_remote_app() - logger.info("Glyph-remote") + logger.info(f"Glyph-remote: Version {version}") app.run(break_condition=bc) diff --git a/glyph/gp/__init__.py b/glyph/gp/__init__.py index ee71113..a3a72ba 100644 --- a/glyph/gp/__init__.py +++ b/glyph/gp/__init__.py @@ -4,6 +4,6 @@ from .algorithms import * from .algorithms import all_algorithms from .breeding import all_crossover, all_mutations -from .constraints import NullSpace, apply_constraints, build_constraints +from .constraints import Constraint, PreTest, PreTestService, NonFiniteExpression, constrain, reject_constrain_violation from .individual import numpy_phenotype, numpy_primitive_set, sympy_phenotype, sympy_primitive_set from .individual import Individual, NDIndividual diff --git a/glyph/gp/constraints.py b/glyph/gp/constraints.py index f8aad8f..6903fb5 100644 --- a/glyph/gp/constraints.py +++ b/glyph/gp/constraints.py @@ -1,16 +1,50 @@ # Copyright: 2017, Markus Abel, Julien Gout, Markus Quade # Licence: LGPL +import pathlib +import sys +import logging +import importlib + +import stopit + from .individual import simplify_this, AExpressionTree -class NullSpace: # todo documentation +logger = logging.getLogger(__name__) + + +class Constraint: + def __init__(self, spaces): + self.spaces = spaces + + def _contains(self, element): + if not self.spaces: + return False + return any(element in subspace for subspace in self.spaces) + + def __contains__(self, element): + #try: + return self._contains(element) + #except Exception as e: + # logger.debug(f"Exception was raised during constraints check: {e}.") + # return False + + +class NonFiniteExpression(Constraint): def __init__(self, zero=True, infty=True, constant=False): + """Use sympy to check for finite expressions. + + Args: + zero: flag to check for zero expressions + infty: flag to check for infinite expressions + constant: flag to check for constant expressions + """ self.zero = zero self.infty = infty self.constant = constant - def __contains__(self, element): + def _contains(self, element): expr = simplify_this(element) if self.constant: if expr.is_constant(): @@ -29,44 +63,94 @@ def __contains__(self, element): return False -def build_constraints(null_space, n_trials=30): +class PreTest(Constraint): + def __init__(self, fn, fun="chi"): + """Apply pre-testing to check for constraint violation. + + The python script needs to provide a callable fun(ind). + + Args: + fn: filename of the python script. + fun: name of the function in fn. + """ + fn = pathlib.Path(fn) + sys.path.append(str(fn.parent)) + try: + mod = importlib.import_module(fn.stem) + self.f = getattr(mod, fun) + except (AttributeError, ImportError): + logger.error(f"Funktion {fun} not available in {fn}") + self.f = lambda *args: False + finally: + sys.path.pop() + + def _contains(self, element): + if self.f is None: + logger.warning("Using invalid PretestNullSpace") + return self.f(element) + + +class PreTestService(Constraint): # todo com cannot be pickled ! + def __init__(self, assessment_runner): + self.assessment_runner = assessment_runner + + @property + def com(self): + return self.assessment_runner.com + + @property + def make_str(self): + return self.assessment_runner.make_str + + def _contains(self, element): + payload = self.make_str(element) + self.com.send(dict(action="PRETEST", payload=payload)) + return self.com.recv()["ok"] == "True" + + +def reject_constrain_violation(constraint, n_trials=30, timeout=60): """Create constraints decorators based on rules. - :param null_space: + :param constraint: :param n_trials: Number of tries. Give up afterwards (return input). :return: list of constraint decorators """ + def reject(operator): def inner(*inds, **kw): - for i in range(n_trials): - out = operator(*inds, **kw) - if isinstance(out, AExpressionTree): # can this be done w/o type checking? - t = out - elif isinstance(out, (list, tuple)): - t = out[0] - else: - raise RuntimeError - if not t in null_space: - break - else: - if inds: - return inds + with stopit.ThreadingTimeout(timeout) as to_ctx: + for i in range(n_trials): + out = operator(*inds, **kw) + if isinstance(out, AExpressionTree): # can this be done w/o type checking? + t = out + elif isinstance(out, (list, tuple)): + t = out[0] + else: + raise RuntimeError + if t not in constraint: + break else: - raise UserWarning + if inds: + return inds + else: + raise UserWarning + if to_ctx.state == to_ctx.TIMED_OUT: + logger.warning(f"Timeout during constrained operation {operator} on individual {inds}.") return out return inner - return [reject] + return reject -def apply_constraints(funcs, constraints): +def constrain(funcs, constraint, n_trials=30, timeout=60): """Decorate a list of genetic operators with constraints. :param funcs: list of operators (mate, mutate, create) - :param constraints: list of constraint decorators + :param constraint: instance of Nullspace :return: constrained operators """ - for c in constraints: - swap = [c(func) for func in funcs] - funcs = swap - return funcs + + if not constraint: + return funcs + return [reject_constrain_violation(constraint, n_trials=n_trials, timeout=timeout)(f) + for f in funcs] diff --git a/glyph/observer.py b/glyph/observer.py index 74e8c29..0abd385 100644 --- a/glyph/observer.py +++ b/glyph/observer.py @@ -1,14 +1,19 @@ +import logging + +import matplotlib import matplotlib.pyplot as plt import numpy as np +logger = logging.getLogger(__name__) + def get_limits(x, factor=1.1): """Calculates the plot range given an array x.""" avg = np.nanmean(x) - l = np.nanmax(x) - np.nanmin(x) - if l == 0: - l = 0.5 - return avg - l/2 * factor, avg + l/2 * factor + range_ = np.nanmax(x) - np.nanmin(x) + if range_ == 0: + range_ = 0.5 + return avg - range_/2 * factor, avg + range_/2 * factor class ProgressObserver(object): # pragma: no cover @@ -16,8 +21,11 @@ def __init__(self): """Animates the progress of the evolutionary optimization. Note: - Uses matplotlibs interactive mode. + Uses matplotlib's interactive mode. """ + logger.debug("The ProgressObserver needs an interactive matplotlib backend.") + logger.debug(f"Using {matplotlib.rcParams['backend']} as backend in matplotlib.") + logger.debug("Try export MPLBACKEND='TkAgg'") plt.ion() self.fig = None self.axis = None @@ -27,9 +35,11 @@ def __init__(self): @staticmethod def _update_plt(ax, line, *data): x, y = data + x = np.array(x) + y = np.array(y) ax.set_xlim(*get_limits(x)) ax.set_ylim(*get_limits(y)) - line.set_data(x, y) + line.set_data(data) def _blank_canvas(self, chapters): self.fig, self.axes = plt.subplots(nrows=len(chapters) + 1) diff --git a/glyph/utils/__init__.py b/glyph/utils/__init__.py index 17cb14a..548f18a 100644 --- a/glyph/utils/__init__.py +++ b/glyph/utils/__init__.py @@ -12,11 +12,12 @@ class Memoize: - """Memoize(fn) - an instance which acts like fn but memoizes its arguments - Will only work on functions with non-mutable arguments - http://code.activestate.com/recipes/52201/ - """ def __init__(self, fn): + """Memoize(fn) - an instance which acts like fn but memoizes its arguments + + Will only work on functions with non-mutable arguments + http://code.activestate.com/recipes/52201/ + """ self.fn = fn self.memo = {} diff --git a/glyph/utils/break_condition.py b/glyph/utils/break_condition.py index 2efbf8d..d08d316 100644 --- a/glyph/utils/break_condition.py +++ b/glyph/utils/break_condition.py @@ -2,18 +2,17 @@ # Licence: LGPL import time -import signal import functools +import stopit import numpy as np class SoftTimeOut: - """Break condition based on a soft time out. - Start a new generation as long as there is some time left. - """ def __init__(self, ttl): - """ + """Break condition based on a soft time out. + + Start a new generation as long as there is some time left. :param ttl: time to live in seconds """ @@ -32,52 +31,8 @@ def __call__(self, *args, **kwargs): return self.alive -def timeout(ttl): - """ Decorate a function. Will raise `TimeourError` if function call takes longer than the ttl. - - Vendored from ffx. - - :param ttl: time to live in seconds - """ - def decorate(f): - def handler(signum, frame): - raise TimeoutError() - - @functools.wraps(f) - def new_f(*args, **kwargs): - old = signal.signal(signal.SIGALRM, handler) - signal.alarm(ttl) - try: - result = f(*args, **kwargs) - finally: - signal.signal(signal.SIGALRM, old) - signal.alarm(0) - return result - return new_f - return decorate - - -def max_fitness_on_timeout(max_fitness): - """ Decorate a function. Associate max_fitness with long running individuals. - - :param max_fitness: fitness of aborted individual calls. - :returns: fitness or max_fitness - """ - def decorate(f): - @functools.wraps(f) - def inner(*args, **kwargs): - try: - fitness = f(*args, **kwargs) - except TimeoutError: - fitness = max_fitness - return fitness - return inner - return decorate - - def soft_max_iter(app, max_iter=np.infty): - """ - Soft breaking condition. Will check after each generation weather maximum number of iterations is exceeded. + """Soft breaking condition. Will check after each generation weather maximum number of iterations is exceeded. :type app: `glyph.application.Application` :param max_iter: maximum number of function evaluations diff --git a/glyph/utils/numeric.py b/glyph/utils/numeric.py index e179878..f2b8ba0 100644 --- a/glyph/utils/numeric.py +++ b/glyph/utils/numeric.py @@ -95,15 +95,13 @@ def f(x): class SmartConstantOptimizer: - """Decorate a minimize method used in `scipy.optimize.minimize` to cancel non promising constant optimizations. + def __init__(self, method, step_size=10, min_stat=10, threshold=25): + """Decorate a minimize method used in `scipy.optimize.minimize` to cancel non promising constant optimizations. - The stopping criteria is based on the improvement rate :math:`\frac{\Delta f}[\Delta fev}`. + The stopping criteria is based on the improvement rate :math:`\frac{\Delta f}[\Delta fev}`. - If the improvement rate is below the :math:`q_{threshold}` quantile for a given number of function - evaluations, optimization is stopped. - """ - def __init__(self, method, step_size=10, min_stat=10, threshold=25): - """ + If the improvement rate is below the :math:`q_{threshold}` quantile for a given number of function + evaluations, optimization is stopped. :params method: see `scipy.optimize.minimize` method :params step_size: number of function evaluations betweem iterations :params min_stat: minimum sample size before stopping diff --git a/requirements-dev.txt b/requirements-dev.txt index 3738449..4b73f71 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,5 +13,6 @@ docutils <0.13.1 networkx codecov sphinx -sphinx-readable-theme +# sphinx-readable-theme +-e git+https://github.com/Ohjeah/sphinx-readable-theme.git#egg=readable sphinxcontrib-apidoc diff --git a/requirements.txt b/requirements.txt index 18a244c..118c770 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pyzmq matplotlib cache.py==0.1.3 deprecated +stopit diff --git a/setup.py b/setup.py index 6d8e144..c8cb41b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ AUTHOR = "Markus Abel, Julien Gout, Markus Quade" KEYWORDS = "complex systems, control, machine learning, genetic programming" LICENCE = "LGPL" -PYTHON = ">=3.5" +PYTHON = ">=3.6" here = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/unittest/gp/test_constraints.py b/tests/unittest/gp/test_constraints.py index 1434d51..7eaaed2 100644 --- a/tests/unittest/gp/test_constraints.py +++ b/tests/unittest/gp/test_constraints.py @@ -1,4 +1,5 @@ from functools import partial +import tempfile import pytest @@ -32,12 +33,11 @@ class SympyTree(AExpressionTree): @pytest.mark.parametrize("case", cases) -def test_nullspace(case): +def test_NonFiniteExpression(case): expr, res, settings, cls = case cls.pset = add_sc(cls.pset, sc_qout) - ns = NullSpace(**settings) + ns = NonFiniteExpression(**settings) ind = cls.from_string(expr) - print(simplify_this(ind).is_constant()) assert (ind in ns) == res @@ -45,16 +45,16 @@ def mock(*inds, ret=None): return [ret] * max(len(inds), 1) -@pytest.mark.parametrize("i", range(3)) # create = 0, mutate = 1, mate = 2 +@pytest.mark.parametrize("i", range(3)) # create = 0, mutate = 1, mate = 2 def test_constraint_decorator(i, NumpyIndividual): ind = NumpyIndividual.from_string("Sub(x_0, x_0)") this_mock = partial(mock, ret=ind) - ns = NullSpace() + ns = NonFiniteExpression() assert ind in ns - [this_mock] = apply_constraints([this_mock], build_constraints(ns)) + [this_mock] = constrain([this_mock], ns) if i == 0: with pytest.raises(UserWarning): @@ -72,8 +72,8 @@ class NDTree(ANDimTree): ind = NumpyIndividual.from_string("Sub(x_0, x_0)") this_mock = partial(mock, ret=ind) - ns = NullSpace() - [this_mock] = apply_constraints([this_mock], build_constraints(ns)) + ns = NonFiniteExpression() + [this_mock] = constrain([this_mock], ns) mate = partial(nd_mutation, mut1d=this_mock) nd_ind = NDTree([ind]*2) @@ -82,3 +82,10 @@ class NDTree(ANDimTree): for c, d in zip(nd_ind, new_nd_ind): assert c == d + + +def test_pretest(): + with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as tmp: + tmp.write("chi = lambda *args: True") + constraint = PreTest(tmp.name) + assert 1 in constraint diff --git a/tests/unittest/test_assessment.py b/tests/unittest/test_assessment.py index 02dd87f..1760601 100644 --- a/tests/unittest/test_assessment.py +++ b/tests/unittest/test_assessment.py @@ -2,6 +2,7 @@ import operator import re +import sys import dill import numpy as np @@ -199,3 +200,20 @@ def test_tuple_wrap(): assert f1 == f(1) g = assessment.tuple_wrap(lambda x: [x]) assert isinstance(g(1), tuple) + + +@pytest.mark.skipif(sys.platform == 'win32', + reason="does not run on windows") +def test_max_fitness_on_timeout(): + + f = lambda: 1 + + def g(): + import time + time.sleep(10) + return f() + + decorator = assessment.max_fitness_on_timeout(2, 1) + + assert decorator(f)() == 1 + assert decorator(g)() == 2 diff --git a/tests/unittest/utils/test_break_condition.py b/tests/unittest/utils/test_break_condition.py index 7bc1b8c..115cc23 100644 --- a/tests/unittest/utils/test_break_condition.py +++ b/tests/unittest/utils/test_break_condition.py @@ -1,5 +1,3 @@ -import sys - import pytest from glyph.utils.break_condition import * @@ -13,32 +11,3 @@ def test_SoftTimeOut(ttl): time.sleep(ttl + 1) assert bool(ttl) != bool(sttl()) - - -@pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") -def test_timeout_decorator(): - ttl = 2 - - @timeout(ttl) - def long_running_function(s): - time.sleep(s) - return True - - with pytest.raises(TimeoutError): - long_running_function(ttl + 1) - - assert long_running_function(ttl - 1) - - -@pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") -def test_max_fitness_on_timeout(): - f = lambda: 1 - def g(): - raise TimeoutError - - decorator = max_fitness_on_timeout(2) - - assert decorator(f)() == 1 - assert decorator(g)() == 2