コード例 #1
0
ファイル: test_utils.py プロジェクト: flying-circus/esec
def test_ConfigDict_access():
    # create a normal dict, feed to ConfigDict
    cfg = ConfigDict(d1)

    # check item access
    assert cfg['k2'] == 'v2'
    cfg['k2'] = '*v2'
    assert cfg['k2'] == '*v2'

    # check nested item access
    assert cfg['k3']['sk2'] == 'sv2'

    # check attribute access
    assert cfg.k1 == 123
    cfg.k4 = '*v4'
    assert cfg.k4 == '*v4'
    assert cfg['k4'] == '*v4'
    # check nested attribute access
    assert cfg.k3.sk1 == 'sv1'

    # check access created attribute to key
    cfg.name = 'Mary'
    cfg.name = 'Fred'
    assert cfg.name == 'Fred'
    assert cfg['name'] == cfg.name, 'Key=Item mismatch?'
コード例 #2
0
ファイル: test_utils.py プロジェクト: flying-circus/esec
def test_ConfigDict_access():
    # create a normal dict, feed to ConfigDict
    cfg = ConfigDict(d1)
    
    # check item access
    assert cfg['k2'] == 'v2'
    cfg['k2'] = '*v2'
    assert cfg['k2'] == '*v2'
    
    # check nested item access
    assert cfg['k3']['sk2'] == 'sv2'
                      
    # check attribute access
    assert cfg.k1 == 123
    cfg.k4 = '*v4'
    assert cfg.k4 == '*v4'
    assert cfg['k4'] == '*v4'
    # check nested attribute access
    assert cfg.k3.sk1 == 'sv1'
    
    # check access created attribute to key
    cfg.name = 'Mary'
    cfg.name = 'Fred'
    assert cfg.name == 'Fred'
    assert cfg['name'] == cfg.name, 'Key=Item mismatch?'
コード例 #3
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def __init__(self, cfg=None):
     '''Performs configuration dictionary overlaying.'''
     self.syntax = merge_cls_dicts(self, 'syntax')  # all in hierarchy
     self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
     if cfg:
         self.cfg.overlay(cfg)  # apply user variables
     cfg_validate(self.cfg,
                  self.syntax,
                  type(self).__name__,
                  warnings=False)
コード例 #4
0
ファイル: __init__.py プロジェクト: flying-circus/esec
    def __init__(self, cfg=None, **other_cfg):
        '''Initialises the landscape.
        
        This method should be overridden to perform any once-off setup
        that is required, such as generating a landscape map or sequence
        of test cases.
        
        :Warn:
            This initialiser is called *before* the system is
            constructed. Importantly, the members of `esec.context` have
            not been initialised and will raise exceptions if accessed.
        '''
        self.syntax = merge_cls_dicts(self, 'syntax')  # all in hierarchy
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))

        if cfg is None: cfg = ConfigDict()
        elif not isinstance(cfg, ConfigDict): cfg = ConfigDict(cfg)

        self.cfg.overlay(cfg)
        for key, value in other_cfg.iteritems():
            if key in self.syntax:
                self.cfg.set_by_name(key, value)
            elif key.partition('_')[0] in self.syntax:
                self.cfg.set_by_name(key.replace('_', '.'), value)

        cfg_validate(self.cfg, self.syntax, self.ltype + ':' + self.lname)

        # Initialise size properties
        self.size = self.cfg.size
        if self.size_equals_parameters and self.cfg.parameters:
            self.size.exact = self.cfg.parameters
        if self.size.exact: self.size.min = self.size.max = self.size.exact
        if self.size.min >= self.size.max:
            self.size.exact = self.size.max = self.size.min

        # Now check for any strict limits (ie parameters)
        cfg_strict_test(self.cfg, self.strict)

        # random seed?
        if not isinstance(self.cfg.random_seed, int):
            random.seed()
            self.cfg.random_seed = cfg.random_seed = random.randint(
                0, sys.maxint)
        self.rand = Random(self.cfg.random_seed)

        # inversion? offset?
        self.invert = self.cfg.invert
        self.offset = self.cfg.offset

        # Autobind _eval_minimise or _eval_maximise.
        if not hasattr(self, 'eval'):
            if self.maximise == self.invert:
                setattr(self, 'eval', self._eval_minimise)
            else:
                setattr(self, 'eval', self._eval_maximise)
コード例 #5
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def by_cfg_str(cls, cfg_str):
     '''Used by test framework to initialise a class instance using a
     simple test string specified in each class.test_cfg as nested
     tuples.
     
     :rtype: Landscape
     '''
     # create ConfigDict using cfg_str (defaults not needed but why not)
     cfg = ConfigDict()
     # map string to appropriate keys and types (or nested keys)
     cfg.set_linear(cls.test_key, cfg_str)
     # provide a new instance
     return cls(cfg)
コード例 #6
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def by_cfg_str(cls, cfg_str):
     '''Used by test framework to initialise a class instance using a
     simple test string specified in each class.test_cfg as nested
     tuples.
     
     :rtype: Landscape
     '''
     # create ConfigDict using cfg_str (defaults not needed but why not)
     cfg = ConfigDict()
     # map string to appropriate keys and types (or nested keys)
     cfg.set_linear(cls.test_key, cfg_str)
     # provide a new instance
     return cls(cfg)
コード例 #7
0
ファイル: Regression.py プロジェクト: flying-circus/esec
def batch():
    for (landscape, dialects, config_overrides) in tests:
        for dialect in dialects:
            if dialect[-2:] == '%d':
                i = 0
                dialect_pattern = dialect
                dialect = dialect_pattern % i
                while dialect in configs:
                    for config_override in config_overrides:
                        cfg = ConfigDict(config)
                        if config_override: cfg.overlay(config_override)
                        yield {
                            'tags': [landscape.partition('.')[0]],
                            'names': '%s+%s' % (landscape, dialect),
                            'config': cfg
                        }
                    i += 1
                    dialect = dialect_pattern % i
            else:
                for config_override in config_overrides:
                    cfg = ConfigDict(config)
                    if config_override: cfg.overlay(config_override)
                    yield {
                        'tags': [landscape.partition('.')[0]],
                        'names': '%s+%s' % (landscape, dialect),
                        'config': cfg
                    }
コード例 #8
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def __init__(self, cfg=None):
     '''Performs configuration dictionary overlaying.'''
     self.syntax = merge_cls_dicts(self, 'syntax') # all in hierarchy
     self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
     if cfg:
         self.cfg.overlay(cfg) # apply user variables
     cfg_validate(self.cfg, self.syntax, type(self).__name__, warnings=False)
コード例 #9
0
 def info(self, level):
     '''Report the current configuration.
     
     .. include:: epydoc_include.txt
     
     :Parameters:
       level : int |ge| 0
         The verbosity level.
     '''
     result = []
     if level > 0:
         result.append('>> System Definition:')
         result.append(self.definition.strip(' \t').strip('\n'))
         result.append('')
     if level > 3:
         result.append('>> Compiled Code:')
         result.append(self._code_string)
         result.append('')
     if level > 2:
         result.append('>> Experiment Configuration:')
         result.extend(self.cfg.list())
     if level > 4:
         result.append('>> System context:')
         result.extend(ConfigDict(self._context).list())
     return result
コード例 #10
0
ファイル: test_utils.py プロジェクト: flying-circus/esec
def test_ConfigDict_validate():
    # check type validate, missing error, unknown warning, optional key ?, any value *
    d1 = {'a': 123, 'b': 12.34, 'c': 's', 'd': 1234,  'x': 123,            'w': 1.0}
    s1 = {'a': int, 'b': float, 'c': str, 'd': float, 'y': int, 'z?': int, 'w?': '*' }
    #   syntax issues     ---->         type-> ^^^^^  ^^^         ^          ^   ^^^ 
    cfg1 = ConfigDict(d1)
    errs, warns, unknown = cfg1.validate(s1)
    assert len(errs) == 2 and len(warns) == 0 and len(unknown) == 1

    # valid and nested, keyword sets
    d2 = {'a':'s', 'b': 123, 'n': {'n1': 's', 'n2': 12.34}, 'k1': 'OK',    'k2': 'BAD'}
    s2 = {'a':str, 'b': int, 'n': {'n1': str, 'n2': int},   'k1': ('OK',), 'k2': ('OK',)}
    #   two issues ------>                                            bad keyword ^^^^^
    # should show nested validation issue and set issue
    cfg2 = ConfigDict(d2)
    errs, warns, unknown = cfg2.validate(s2)
    assert len(errs) == 1 and len(warns) == 1 and len(unknown) == 0
コード例 #11
0
    def __init__(self, cfg, eval_default):
        '''Initialises a new `Species` instance.
        
        :Parameters:
          cfg : dict, `ConfigDict`
            The set of configuration options applying to this species.
            No syntax is provided by the `Species` base class, but
            derived classes may require certain parameters.
          
          eval_default : evaluator
            The default evaluator for `Individual` instances of this
            species. Evaluators provide a method `eval` taking a single
            individual as a parameter.
        '''
        # Merge syntax and default details
        self.syntax = utils.merge_cls_dicts(self, 'syntax')
        self.cfg = ConfigDict(utils.merge_cls_dicts(self, 'default'))
        # Now apply user cfg details and test against syntax
        self.cfg.overlay(cfg)
        utils.cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
        # Store default evaluator
        self._eval_default = eval_default
        '''The default evaluator for individuals of this species type.'''
        # Initialise public_context if necessary
        if not hasattr(self, 'public_context'):
            self.public_context = {}
            '''Items to include in a system's execution context. This
            typically contains references to the initialisers provided
            by the species.'''

        # Set some default properties to imitiate Individual
        self.species = self
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual.species
        '''
        self._eval = eval_default
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual._eval
        '''
        self.statistic = {}
        '''Provided to make `Species` and `Individual` trivially
コード例 #12
0
ファイル: test_utils.py プロジェクト: flying-circus/esec
def test_ConfigDict_overlay():
    # overlay
    cfg1 = ConfigDict({'a': 1, 'b': 2, 'c': {'x': 1, 'y': 2}})
    cfg2 = ConfigDict({
        'f': 8,
        'b': 3,
        'c': {
            'x': 5,
        }
    })
    assert str(cfg1) == '{a:1, b:2, c:{x:1, y:2}}', str(cfg1)
    assert str(cfg2) == '{b:3, c:{x:5}, f:8}', str(cfg2)
    assert (cfg1 == cfg2) == False
    cfg3 = ConfigDict(cfg1)
    assert (cfg1 == cfg3) == True
    cfg4 = cfg1 + cfg2
    cfg1 += cfg2
    assert (cfg4 == cfg1) == True
コード例 #13
0
ファイル: Regression.py プロジェクト: flying-circus/esec
def batch():
    for (landscape, dialects, config_overrides) in tests:
        for dialect in dialects:
            if dialect[-2:] == '%d':
                i = 0
                dialect_pattern = dialect
                dialect = dialect_pattern % i
                while dialect in configs:
                    for config_override in config_overrides:
                        cfg = ConfigDict(config)
                        if config_override: cfg.overlay(config_override)
                        yield {
                            'tags': [landscape.partition('.')[0]],
                            'names': '%s+%s' % (landscape, dialect),
                            'config': cfg
                        }
                    i += 1
                    dialect = dialect_pattern % i
            else:
                for config_override in config_overrides:
                    cfg = ConfigDict(config)
                    if config_override: cfg.overlay(config_override)
                    yield {
                        'tags': [landscape.partition('.')[0]],
                        'names': '%s+%s' % (landscape, dialect),
                        'config': cfg
                    }
コード例 #14
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def __init__(self, cfg=None, **other_cfg):
     '''Initialises the landscape.
     
     This method should be overridden to perform any once-off setup
     that is required, such as generating a landscape map or sequence
     of test cases.
     
     :Warn:
         This initialiser is called *before* the system is
         constructed. Importantly, the members of `esec.context` have
         not been initialised and will raise exceptions if accessed.
     '''
     self.syntax = merge_cls_dicts(self, 'syntax') # all in hierarchy
     self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
     
     if cfg is None: cfg = ConfigDict()
     elif not isinstance(cfg, ConfigDict): cfg = ConfigDict(cfg)
     
     self.cfg.overlay(cfg)
     for key, value in other_cfg.iteritems():
         if key in self.syntax:
             self.cfg.set_by_name(key, value)
         elif key.partition('_')[0] in self.syntax:
             self.cfg.set_by_name(key.replace('_', '.'), value)
     
     cfg_validate(self.cfg, self.syntax, self.ltype + ':' + self.lname)
     
     # Initialise size properties
     self.size = self.cfg.size
     if self.size_equals_parameters and self.cfg.parameters: self.size.exact = self.cfg.parameters
     if self.size.exact: self.size.min = self.size.max = self.size.exact
     if self.size.min >= self.size.max: self.size.exact = self.size.max = self.size.min
     
     # Now check for any strict limits (ie parameters)
     cfg_strict_test(self.cfg, self.strict)
     
     # random seed?
     if not isinstance(self.cfg.random_seed, int):
         random.seed()
         self.cfg.random_seed = cfg.random_seed = random.randint(0, sys.maxint)
     self.rand = Random(self.cfg.random_seed)
     
     # inversion? offset?
     self.invert = self.cfg.invert
     self.offset = self.cfg.offset
     
     # Autobind _eval_minimise or _eval_maximise.
     if not hasattr(self, 'eval'):
         if self.maximise == self.invert:
             setattr(self, 'eval', self._eval_minimise)
         else:
             setattr(self, 'eval', self._eval_maximise)
コード例 #15
0
ファイル: run.py プロジェクト: flying-circus/esec
def _load_config(config_string, defaults):
    '''Loads a configuration from a configuration string.'''
    cfg = ConfigDict(defaults)
    for name in (o for o in config_string.split('+') if o):
        # Get name from configs
        if name in configs: 
            cfg.overlay(configs[name])
        # Get name from current configuration
        elif name in cfg:
            cfg.overlay(cfg[name])
        # Attempt to load module from cfgs or plugins
        else:
            mod = _load_module('cfgs', name) or _load_module('plugins', name) or _load_module(None, name)
            if not mod:
                raise ImportError('Cannot find ' + name + ' as configuration or plugin.')
            
            mod_cfg1 = mod.get('configs', None)
            mod_def = mod.get('defaults', None)
            mod_cfg2 = mod.get('config', None)
            if mod_cfg1: configs.update(mod_cfg1)
            if mod_def: cfg.overlay(mod_def)
            if mod_cfg2: cfg.overlay(mod_cfg2)
    return cfg
コード例 #16
0
ファイル: __init__.py プロジェクト: flying-circus/esec
 def __init__(self, cfg, eval_default):
     '''Initialises a new `Species` instance.
     
     :Parameters:
       cfg : dict, `ConfigDict`
         The set of configuration options applying to this species.
         No syntax is provided by the `Species` base class, but
         derived classes may require certain parameters.
       
       eval_default : evaluator
         The default evaluator for `Individual` instances of this
         species. Evaluators provide a method `eval` taking a single
         individual as a parameter.
     '''
     # Merge syntax and default details
     self.syntax = utils.merge_cls_dicts(self, 'syntax')
     self.cfg = ConfigDict(utils.merge_cls_dicts(self, 'default'))
     # Now apply user cfg details and test against syntax
     self.cfg.overlay(cfg)
     utils.cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
     # Store default evaluator
     self._eval_default = eval_default
     '''The default evaluator for individuals of this species type.'''
     # Initialise public_context if necessary
     if not hasattr(self, 'public_context'):
         self.public_context = { }
         '''Items to include in a system's execution context. This
         typically contains references to the initialisers provided
         by the species.'''
     
     # Set some default properties to imitiate Individual
     self.species = self
     '''Provided to make `Species` and `Individual` trivially
     compatible.
     
     :see: Individual.species
     '''
     self._eval = eval_default
     '''Provided to make `Species` and `Individual` trivially
     compatible.
     
     :see: Individual._eval
     '''
     self.statistic = { }
     '''Provided to make `Species` and `Individual` trivially
コード例 #17
0
ファイル: test_utils.py プロジェクト: flying-circus/esec
def test_ConfigDict_validate():
    # check type validate, missing error, unknown warning, optional key ?, any value *
    d1 = {'a': 123, 'b': 12.34, 'c': 's', 'd': 1234, 'x': 123, 'w': 1.0}
    s1 = {
        'a': int,
        'b': float,
        'c': str,
        'd': float,
        'y': int,
        'z?': int,
        'w?': '*'
    }
    #   syntax issues     ---->         type-> ^^^^^  ^^^         ^          ^   ^^^
    cfg1 = ConfigDict(d1)
    errs, warns, unknown = cfg1.validate(s1)
    assert len(errs) == 2 and len(warns) == 0 and len(unknown) == 1

    # valid and nested, keyword sets
    d2 = {
        'a': 's',
        'b': 123,
        'n': {
            'n1': 's',
            'n2': 12.34
        },
        'k1': 'OK',
        'k2': 'BAD'
    }
    s2 = {
        'a': str,
        'b': int,
        'n': {
            'n1': str,
            'n2': int
        },
        'k1': ('OK', ),
        'k2': ('OK', )
    }
    #   two issues ------>                                            bad keyword ^^^^^
    # should show nested validation issue and set issue
    cfg2 = ConfigDict(d2)
    errs, warns, unknown = cfg2.validate(s2)
    assert len(errs) == 1 and len(warns) == 1 and len(unknown) == 0
コード例 #18
0
ファイル: __init__.py プロジェクト: flying-circus/esec
class MonitorBase(object):
    '''Defines the base class for monitors to be used with ESDL defined
    systems.
    
    `MonitorBase` can also be used as a 'do-nothing' monitor.
    
    While `MonitorBase` does not make use of it, configuration
    dictionaries are supported. The default initialiser accepts a
    configuration which it will overlay over child ``syntax`` and
    ``default`` dictionaries.
    '''
    
    syntax = { }
    default = { }
    
    def __init__(self, cfg=None):
        '''Performs configuration dictionary overlaying.'''
        self.syntax = merge_cls_dicts(self, 'syntax') # all in hierarchy
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
        if cfg:
            self.cfg.overlay(cfg) # apply user variables
        cfg_validate(self.cfg, self.syntax, type(self).__name__, warnings=False)
    
    
    _required_methods = [
        'on_yield',
        'on_notify', 'notify',
        'on_pre_reset', 'on_post_reset',
        'on_pre_breed', 'on_post_breed',
        'on_run_start', 'on_run_end',
        'on_exception',
        'should_terminate'
    ]
    
    @classmethod
    def isinstance(cls, inst):
        '''Returns ``True`` if `inst` is compatible with `MonitorBase`.
        
        An object is considered compatible if it is a subclass of
        `MonitorBase`, or if it implements the same methods. Methods are
        not tested for signatures, which may result in errors occurring
        later in the program.
        '''
        if isinstance(inst, cls): return True
        if inst is None: return False
        return all(hasattr(inst, method) for method in cls._required_methods)
    
    def on_yield(self, sender, name, group):
        '''Called for each population YIELDed in the system.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_post_breed` will be called and if
        `should_terminate` returns ``False`` execution will continue
        normally.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_notify(self, sender, name, value):
        '''Called in response to notifications from other objects. For
        example, a mutation operation may call ``notify`` to report to
        the monitor how many individuals were mutated. The monitor
        receives this message through `on_notify` and either ignores it
        or retains the statistic.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_post_breed` will be called and if
        `should_terminate` returns ``False`` execution will continue
        normally.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the call
            to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        '''
        pass
    
    def _on_notify(self, sender, name, value):
        '''Handles notification messages.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the call
            to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        
        :Warn:
            Do not override this method to handle messages.
            That is what `on_notify` is for. Only override this
            method if you are implementing a queuing or
            synchronisation mechanism.
        '''
        self.on_notify(sender, name, value)
    
    def notify(self, sender, name, value):
        '''Sends a notification message to this monitor. This is used in
        contexts where a reference to the monitor is readily available.
        If the global ``notify`` function is available (for example, in
        selectors, generators or evaluators) it should be used instead.
        
        The global ``notify`` function can be obtained by importing
        `esec.context.notify`.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the
            call to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        '''
        self._on_notify(sender, name, value)
    
    def on_pre_reset(self, sender):
        '''Called when the groups are reset, generally immediately after
        `on_run_start` is called.
        
        If this function or the system reset raises an exception, it
        will be passed to `on_exception`, `on_run_end` will be called
        and the run will be terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_post_reset(self, sender):
        '''Called immediately after the initialisation code specified in
        the system definition has executed.
        
        If this function or the system reset raises an exception, it
        will be passed to `on_exception`, `on_run_end` will be called
        and the run will be terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_pre_breed(self, sender):
        '''Called before breeding the current generation.
        
        If this function or the system breed raises an exception, it
        will be passed to `on_exception`, `on_post_breed` will be called
        and if `should_terminate` returns ``False`` execution will
        continue normally.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_post_breed(self, sender):
        '''Called after breeding the current generation.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_run_start(self, sender):
        '''Called at the beginning of a run, before `on_pre_reset`.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_run_end` will be called and the run will be
        terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_run_end(self, sender):
        '''Called at the end of a run, regardless of the reason for
        ending.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass
    
    def on_exception(self, sender, exception_type, value, trace):
        '''Called when an exception is thrown.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
          
          exception_type : type(Exception)
            The type object representing the exception that was raised.
          
          value : Exception object
            The exception object that was raised.
          
          trace : string
            A displayable exception message formatted using the
            ``traceback`` module.
        '''
        pass
    
    def should_terminate(self, sender):     #pylint: disable=R0201,W0613
        '''Called after each experiment step to determine whether to
        terminate.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        
        :Returns:
            ``True`` if the run should terminate immediately; otherwise,
            ``False``.
        '''
        return True
コード例 #19
0
ファイル: experiment.py プロジェクト: flying-circus/esec
class Experiment(object):
    '''Used to conduct an experiment using a specified breeding system
    over a given landscape.
    
    This class is instatiated with a dictionary matching `syntax`.
    '''
    
    syntax = {
        'random_seed': [int, None],
        'monitor': '*', # pre-initialised MonitorBase instance, class or dict
        'landscape': '*',
        'system': '*', # allow System to validate
        'selector?': '*', # System also validates this
        'verbose': int,
    }
    '''The expected format of the configuration dictionary passed to
    `__init__`.
    
    .. include:: epydoc_include.txt
    
    Members:
      random_seed : (int [optional])
        The seed value to use for random number generation in the
        system. Landscapes use their own random number generators and
        may be seeded independently.
      
      monitor : (`MonitorBase` instance, subclass or dictionary)
        The monitor to use for the experiment. If it is an instance of a
        class derived from `MonitorBase`, it is used without
        modification. If it is a subclass of `MonitorBase`, it is
        instantiated with no parameters.
        
        If it is a dictionary, it must have either a key ``instance`` or
        a key ``class``. The ``instance`` key contains an instance of
        `MonitorBase`. The ``class`` key contains a subclass of
        `MonitorBase` that will be instantiated with the relevant
        configuration dictionary (from ``monitor`` downwards).
        
        If a valid value is not provided, a `ValueError` is raised.
      
      landscape : (`Landscape` instance, subclass or dictionary)
        The landscape to use for the experiment. If it is an instance of
        a class derived from `Landscape`, it is used without
        modification. If it is a subclass of `Landscape`, it is
        instantiated with no parameters.
        
        If it is a dictionary, it must have either a key ``instance`` or
        a key ``class``. The ``instance`` key contains an instance of
        `Landscape`. The ``class`` key contains a subclass of
        `Landscape` that will be instantiated with the relevant
        configuration dictionary (from ``landscape`` downwards).
        
        If a valid value is not provided, a `ValueError` is raised.
      
      system : (dictionary)
        The definition of the system. This includes the key
        ``definition``, which is the ESDL text to compile. Any other
        values provided in ``system`` are made available to the system.
        
        Keys in ``system`` should not start with an underscore, since
        these names are reserved for use by the ESDL compiler and
        runtime.
      
      verbose : (int |ge| 0 [defaults to zero])
        The verbosity level to use.
    
    '''
    
    
    default = {
        'verbose': 0,
        'random_seed': None,
    }
    '''The default values to use for unspecified keys in `syntax`.
    '''
    
    def _load(self, cfg, key, base=None, attr=None):
        '''Returns the object provided in `key` of `cfg` if it is
        derived from `base`.
        
        If not, looks for ``instance`` within `key` and returns that. If
        that fails, instantiates ``class`` within `key` with ``cfg.key``
        and returns that.
        
        If everything fails, returns ``None``.
        '''
        obj = cfg_read(cfg, key)
        if isinstance(obj, base):
            # value is the object
            return obj
        elif isinstance(obj, type) and issubclass(obj, base):
            # value is a class with no configuration
            return obj()
        elif isinstance(obj, (dict, ConfigDict)):
            # try loading .instance
            obj_ins = self._load(cfg, key + '.instance', base)
            if obj_ins: return obj_ins
            # try loading .class (with base == type)
            obj_cls = self._load(cfg, key + '.class', type)
            if obj_cls and issubclass(obj_cls, base):
                # instantiate class with config
                return obj_cls(obj)
        elif attr and hasattr(obj, attr):
            # value is the object
            return obj
        
        # all failed, return None
        return None
    
    def __init__(self, cfg):
        '''Initialises a new experiment with configuration dictionary
        `cfg`. `cfg` must match the syntax given in `syntax`.
        
        :Exceptions:
          - `ValueError`: Unusable values were passed in ``monitor`` or
            ``landscape``. See `syntax` for a description of what
            constitutes a valid value.
          
          - `ESDLCompilerError`: One or more errors occurred while
            compiling the provided system. Access the
            ``validation_result`` member of the exception object for
            specific information about each error.
        
        :Note:
            All exceptions are re-raised by this constructor. Apart
            from `KeyboardInterrupt`, exceptions raised after the
            monitor is available are passed to the
            `MonitorBase.on_exception` handler first.
        '''
        # Configuration processing...
        self.cfg = ConfigDict(self.default)
        # Overlay the supplied cfg onto defaults
        self.cfg.overlay(cfg)
        # -- Validate cfg against syntax
        cfg_validate(self.cfg, self.syntax, 'Experiment', warnings=True)
        
        # hide the user provided cfg with validated self.cfg
        cfg = self.cfg
        
        # -- Monitor --
        self.monitor = self._load(cfg, 'monitor', MonitorBase, 'should_terminate')
        if not MonitorBase.isinstance(self.monitor):
            raise TypeError('No monitor provided.')
        cfg.monitor = self.monitor
        
        try:
            # random seed?
            try:
                self.random_seed = int(cfg.random_seed)
            except TypeError:
                random.seed()
                self.random_seed = cfg.random_seed = random.randrange(0, sys.maxint)
        
            # -- Landscape (of type and name) --
            self.lscape = self._load(cfg, 'landscape', landscape.Landscape, 'eval')
            cfg.landscape = self.lscape
        
            # -- Pass full configuration to monitor --
            self.monitor.notify('Experiment', 'Configuration', cfg)

            # -- System --
            self.system = System(cfg, self.lscape, self.monitor)

            # -- Pass compiled system and landscape to monitor --
            self.monitor.notify('Experiment', 'System', self.system)
            if self.lscape: self.monitor.notify('Experiment', 'Landscape', self.lscape)
        except KeyboardInterrupt:
            raise
        except:
            ex = sys.exc_info()
            ex_type, ex_value = ex[0], ex[1]
            if ex_type is EvaluatorError:
                ex_type, ex_value, ex_trace = ex_value.args
            elif ex_type is ESDLCompilerError:
                ex_trace = '\n'.join(str(i) for i in ex_value.validation_result.all)
            elif ex_type is ExceptionGroup:
                ex_trace = '\n'.join(str(i) for i in ex_value.exceptions)
            else:
                ex_trace = ''.join(traceback.format_exception(*ex))
            self.monitor.on_exception(self, ex_type, ex_value, ex_trace)
            raise
    
    
    def run(self):
        '''Run the experiment.'''
        self.begin()
        
        while self.step(): pass
        
        self.close()
    
    def begin(self):
        '''Start the experiment.'''
        self.system.begin()
    
    def step(self, always_step=False):
        '''Executes the next step in the experiment. If the monitor's
        ``should_terminate`` callback returns ``True``, the step is not
        executed unless `always_step` is ``True``.
        
        :Parameters:
          always_step : bool
            ``True`` to execute the step, even if the monitor's
            ``should_terminate`` callback returns ``True``.
        
        :Returns:
            ``True`` if the monitor does not indicate that it should
            terminate (that is, ``should_terminate`` returns ``False``).
            This value is unaffected by `always_step`.
        '''
        
        if self.monitor.should_terminate(self.system):  #pylint: disable=E1103
            if always_step: self.system.step()
            return False
        else:
            self.system.step()
            return True
    
    def close(self):
        '''Closes the experiment.'''
        self.system.close()
コード例 #20
0
ファイル: experiment.py プロジェクト: flying-circus/esec
    def __init__(self, cfg):
        '''Initialises a new experiment with configuration dictionary
        `cfg`. `cfg` must match the syntax given in `syntax`.
        
        :Exceptions:
          - `ValueError`: Unusable values were passed in ``monitor`` or
            ``landscape``. See `syntax` for a description of what
            constitutes a valid value.
          
          - `ESDLCompilerError`: One or more errors occurred while
            compiling the provided system. Access the
            ``validation_result`` member of the exception object for
            specific information about each error.
        
        :Note:
            All exceptions are re-raised by this constructor. Apart
            from `KeyboardInterrupt`, exceptions raised after the
            monitor is available are passed to the
            `MonitorBase.on_exception` handler first.
        '''
        # Configuration processing...
        self.cfg = ConfigDict(self.default)
        # Overlay the supplied cfg onto defaults
        self.cfg.overlay(cfg)
        # -- Validate cfg against syntax
        cfg_validate(self.cfg, self.syntax, 'Experiment', warnings=True)
        
        # hide the user provided cfg with validated self.cfg
        cfg = self.cfg
        
        # -- Monitor --
        self.monitor = self._load(cfg, 'monitor', MonitorBase, 'should_terminate')
        if not MonitorBase.isinstance(self.monitor):
            raise TypeError('No monitor provided.')
        cfg.monitor = self.monitor
        
        try:
            # random seed?
            try:
                self.random_seed = int(cfg.random_seed)
            except TypeError:
                random.seed()
                self.random_seed = cfg.random_seed = random.randrange(0, sys.maxint)
        
            # -- Landscape (of type and name) --
            self.lscape = self._load(cfg, 'landscape', landscape.Landscape, 'eval')
            cfg.landscape = self.lscape
        
            # -- Pass full configuration to monitor --
            self.monitor.notify('Experiment', 'Configuration', cfg)

            # -- System --
            self.system = System(cfg, self.lscape, self.monitor)

            # -- Pass compiled system and landscape to monitor --
            self.monitor.notify('Experiment', 'System', self.system)
            if self.lscape: self.monitor.notify('Experiment', 'Landscape', self.lscape)
        except KeyboardInterrupt:
            raise
        except:
            ex = sys.exc_info()
            ex_type, ex_value = ex[0], ex[1]
            if ex_type is EvaluatorError:
                ex_type, ex_value, ex_trace = ex_value.args
            elif ex_type is ESDLCompilerError:
                ex_trace = '\n'.join(str(i) for i in ex_value.validation_result.all)
            elif ex_type is ExceptionGroup:
                ex_trace = '\n'.join(str(i) for i in ex_value.exceptions)
            else:
                ex_trace = ''.join(traceback.format_exception(*ex))
            self.monitor.on_exception(self, ex_type, ex_value, ex_trace)
            raise
コード例 #21
0
ファイル: __init__.py プロジェクト: flying-circus/esec
class Species(object):
    '''Abstract base class for species descriptors.
    '''
    
    _include_automatically = True
    '''Indicates whether the class should be included in the set of
    available species. If ``True``, the species is instantiated for
    every system and the contents of `public_context` is merged into the
    system context.
    
    This only applies to classes deriving from `Species` *and* included
    in `esec.species`. Other species classes are never included
    automatically.
    '''
    
    name = 'N/A'
    '''The display name of the species class.
    '''
    
    def __init__(self, cfg, eval_default):
        '''Initialises a new `Species` instance.
        
        :Parameters:
          cfg : dict, `ConfigDict`
            The set of configuration options applying to this species.
            No syntax is provided by the `Species` base class, but
            derived classes may require certain parameters.
          
          eval_default : evaluator
            The default evaluator for `Individual` instances of this
            species. Evaluators provide a method `eval` taking a single
            individual as a parameter.
        '''
        # Merge syntax and default details
        self.syntax = utils.merge_cls_dicts(self, 'syntax')
        self.cfg = ConfigDict(utils.merge_cls_dicts(self, 'default'))
        # Now apply user cfg details and test against syntax
        self.cfg.overlay(cfg)
        utils.cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
        # Store default evaluator
        self._eval_default = eval_default
        '''The default evaluator for individuals of this species type.'''
        # Initialise public_context if necessary
        if not hasattr(self, 'public_context'):
            self.public_context = { }
            '''Items to include in a system's execution context. This
            typically contains references to the initialisers provided
            by the species.'''
        
        # Set some default properties to imitiate Individual
        self.species = self
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual.species
        '''
        self._eval = eval_default
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual._eval
        '''
        self.statistic = { }
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual.statistic
        '''
    
    def legal(self, indiv): #pylint: disable=W0613,R0201
        '''Determines whether the specified individual is legal.
        
        By default, this function always returns ``True``. Subclasses
        may override this to perform range or bounds checking or other
        verification appropriate to the species.
        
        :See: esec.individual.Individual.legal
        '''
        return True
    
    #pylint: disable=R0201
    def mutate_insert(self, _source,
                      per_indiv_rate=1.0,
                      length=None, shortest=1, longest=10,
                      longest_result=None):
        '''Mutates a group of individuals by inserting random gene
        sequences.
        
        Gene sequences are created by using the ``init_random`` method
        provided by the derived species type. This ``init_random``
        method must include a parameter named ``template`` which
        receives an individual to obtain bounds and values from.
        
        This method should be overridden for species that don't support
        random insertion directly into the genome.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken one at a
            time from this sequence and either returned unaltered or
            cloned and mutated.
          
          per_indiv_rate : |prob|
            The probability of any individual being mutated. If an
            individual is not mutated, it is returned unmodified.
          
          length : int > 0 [optional]
            The number of genes to insert at each mutation. If left
            unspecified, a random number between `shortest` and
            `longest` (inclusive) is used to determine the length.
          
          shortest : int > 0
            The smallest number of genes that may be inserted at any
            mutation.
          
          longest : int > `shortest`
            The largest number of genes that may be inserted at any
            mutation.
          
          longest_result : int > 0 [optional]
            The longest new genome that may be created. The length of
            the inserted segment is deliberately selected to avoid
            creating genomes longer than this. If there is no way to
            avoid creating a longer genome, the original individual
            is returned and an ``'aborted'`` notification is sent to
            the monitor from ``'mutate_insert'``.
        '''
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert length is not True, "length has no value"
        assert shortest is not True, "shortest has no value"
        assert longest is not True, "longest has no value"
        assert longest_result is not True, "longest_result has no value"
        
        if length is not None: shortest = longest = length
        
        shortest = int(shortest)
        longest = int(longest)
        longest_result = int(longest_result or maxsize)
        
        assert longest >= shortest, \
               "Value of longest (%d) must be higher or equal to shortest (%d)" % (longest, shortest)
        
        frand = rand.random
        irand = rand.randrange
        
        do_all_indiv = (per_indiv_rate >= 1.0)
        
        for indiv in _source:
            if do_all_indiv or frand() < per_indiv_rate:
                len_indiv = len(indiv.genome)
                cut = irand(len_indiv)
                lmax = (longest) if (len_indiv + longest < longest_result) else (longest_result - len_indiv)
                indrand = indiv.init_random(length=longest, template=indiv)
                if lmax >= shortest:
                    insert = next(indrand)[:irand(shortest, lmax+1)]
                    stats = { 'mutated': 1, 'inserted_genes': len(insert) }
                    yield type(indiv)(indiv.genome[:cut] + insert + indiv.genome[cut:], indiv, statistic=stats)
                else:
                    value = { 'i': indiv, 'longest_result': longest_result }
                    notify('mutate_insert', 'aborted', value)
                    yield indiv
            else:
                yield indiv
    
    #pylint: disable=R0201
    def mutate_delete(self, _source,
                      per_indiv_rate=1.0,
                      length=None, shortest=1, longest=10,
                      shortest_result=1):
        '''Mutates a group of individuals by deleting random gene
        sequences.
        
        The number of genes to delete is selected randomly. If this
        value is the same as the number of genes in the individual, all
        but the first `shortest_result` genes are deleted.
        
        This method should be overridden for species that don't support
        random deletion directly from the ``genome`` property.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken one at a
            time from this sequence and either returned unaltered or
            cloned and mutated.
          
          per_indiv_rate : |prob|
            The probability of any individual being mutated. If an
            individual is not mutated, it is returned unmodified.
          
          length : int > 0 [optional]
            The number of genes to delete at each mutation. If left
            unspecified, a random number between `shortest` and
            `longest` (inclusive) is used to determine the length.
          
          shortest : int > 0
            The smallest number of genes that may be deleted at any
            mutation.
          
          longest : int > `shortest`
            The largest number of genes that may be deleted at any
            mutation.
          
          shortest_result : int > 0
            The shortest new genome that may be created. The length
            of the deleted segment is deliberately selected to avoid
            creating genomes shorter than this. If the original
            individual is this length or shorter, it is returned
            unmodified.
        '''
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert length is not True, "length has no value"
        assert shortest is not True, "shortest has no value"
        assert longest is not True, "longest has no value"
        assert shortest_result is not True, "shortest_result has no value"
        
        if length is not None: shortest = longest = length
        
        shortest = int(shortest)
        longest = int(longest)
        shortest_result = int(shortest_result)
        
        assert longest >= shortest, \
               "Value of longest (%d) must be higher or equal to shortest (%d)" % (longest, shortest)
        
        frand = rand.random
        irand = rand.randrange
        
        do_all_indiv = (per_indiv_rate >= 1.0)
        
        for indiv in _source:
            len_indiv = len(indiv.genome)
            if len_indiv > shortest_result and (do_all_indiv or frand() < per_indiv_rate):
                lmax = len_indiv - shortest_result
                if lmax > longest: lmax = longest
                length = irand(shortest, lmax+1) if lmax >= shortest else len_indiv
                if length < len_indiv:
                    cut1 = irand(len_indiv - length)
                    cut2 = cut1 + length
                    stats = { 'mutated': 1, 'deleted_genes': length }
                    yield type(indiv)(indiv.genome[:cut1] + indiv.genome[cut2:], indiv, statistic=stats)
                else:
                    new_indiv = indiv.genome[:shortest_result]
                    deleted = len_indiv - len(new_indiv)
                    if deleted:
                        yield type(indiv)(new_indiv, indiv, statistic={ 'mutated': 1, 'deleted_genes': deleted })
                    else:
                        yield indiv
            else:
                yield indiv
    
    def crossover_uniform(self, _source,
                          per_pair_rate=None, per_indiv_rate=1.0, per_gene_rate=0.5,
                          genes=None, discrete=False,
                          one_child=True, two_children=False):
        '''Performs uniform crossover by selecting genes at random from
        one of two individuals.
        
        Returns a sequence of crossed individuals based on the individuals
        in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a time
            from this sequence, recombined to produce two new individuals,
            and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          per_gene_rate : |prob|
            The probability of any particular pair of genes being swapped.
          
          discrete : bool
            If ``True``, uses discrete recombination, where source genes
            may be copied rather than exchanged, resulting in the same
            value appearing in both offspring.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert per_gene_rate is not True, "per_gene_rate has no value"
        assert genes is not True, "genes has no value"
        
        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or (per_gene_rate <= 0.0 and not genes):
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration
        
        do_all_pairs = (per_pair_rate >= 1.0)
        do_all_genes = (per_gene_rate >= 1.0)
        
        frand = rand.random
        
        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)
                
                new_genes1 = list(i1_genome)
                new_genes2 = list(i2_genome)
                source = xrange(i1_len if i1_len < i2_len else i2_len)

                if genes:
                    do_all_genes = True
                    source = list(source)
                    rand.shuffle(source)
                    source = islice(source, genes)

                for i in source:
                    if do_all_genes or frand() < per_gene_rate:
                        if discrete:
                            new_genes1[i] = i1_genome[i] if frand() < 0.5 else i2_genome[i]
                            new_genes2[i] = i1_genome[i] if frand() < 0.5 else i2_genome[i]
                        else:
                            new_genes1[i] = i2_genome[i]
                            new_genes2[i] = i1_genome[i]
                
                i1 = type(i1)(new_genes1, i1, statistic={ 'recombined': 1 })
                i2 = type(i2)(new_genes2, i2, statistic={ 'recombined': 1 })
            
            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2

    def crossover_discrete(self, _source,
                           per_pair_rate=None, per_indiv_rate=1.0, per_gene_rate=1.0,
                           one_child=True, two_children=False):
        '''A specialisation of `crossover_uniform` for discrete
        crossover.
        
        Note that `crossover_discrete` has a different default value for
        `per_gene_rate` to `crossover_uniform`.
        '''
        return self.crossover_uniform(
            _source=_source,
            per_pair_rate=per_pair_rate, per_indiv_rate=per_indiv_rate,
            per_gene_rate=per_gene_rate,
            discrete=True,
            one_child=one_child, two_children=two_children)
    
    def crossover(self, _source,
                  points=1,
                  per_pair_rate=None, per_indiv_rate=1.0,
                  one_child=True, two_children=False):
        '''Performs crossover by selecting a `points` points common to
        both individuals and exchanging the sequences of genes to the
        right (including the selection).
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          points : int |ge| 1
            The number of points to cross at. If zero, individuals are
            returned unmodified (respecting the setting of
            `one_child`/`two_children`). If greater than the length of
            the individual, every gene will be exchanged.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is.
        '''
        assert points is not True, "points has no value"
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        
        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or points < 1:
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration
        
        do_all_pairs = (per_pair_rate >= 1.0)
        points = int(points)
        
        frand = rand.random
        shuffle = rand.shuffle
        
        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)
                
                if i1_len > points and i2_len > points:
                    max_len = i1_len if i1_len < i2_len else i2_len
                    cuts = list(xrange(1, max_len))
                    shuffle(cuts)
                    cuts = list(sorted(islice(cuts, points)))
                    cuts.append(max_len)
                    
                    new_genes1 = list(i1_genome)
                    new_genes2 = list(i2_genome)
                    
                    for cut_i, cut_j in utils.pairs(iter(cuts)):
                        cut1 = islice(new_genes1, cut_i, cut_j)
                        cut2 = islice(new_genes2, cut_i, cut_j)
                        new_genes1 = list(chain(islice(new_genes1, cut_i),
                                                cut2,
                                                islice(new_genes1, cut_j, None)))
                        new_genes2 = list(chain(islice(new_genes2, cut_i),
                                                cut1,
                                                islice(new_genes2, cut_j, None)))
                    
                    i1 = type(i1)(new_genes1, i1, statistic={ 'recombined': 1 })
                    i2 = type(i2)(new_genes2, i2, statistic={ 'recombined': 1 })
            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2
    
    def crossover_one(self, _source,
                      per_pair_rate=None, per_indiv_rate=1.0,
                      one_child=True, two_children=False):
        '''A specialisation of `crossover` for single-point crossover.
        '''
        return self.crossover(
            _source,
            points=1,
            per_pair_rate=per_pair_rate, per_indiv_rate=per_indiv_rate,
            one_child=one_child, two_children=two_children)
    
    def crossover_two(self, _source,
                      per_pair_rate=None, per_indiv_rate=1.0,
                      one_child=True, two_children=False):
        '''A specialisation of `crossover` for two-point crossover.'''
        return self.crossover(
            _source,
            points=2,
            per_pair_rate=per_pair_rate, per_indiv_rate=per_indiv_rate,
            one_child=one_child, two_children=two_children)
    
    def crossover_different(self, _source,  #pylint: disable=R0915
                            points=1,
                            per_pair_rate=None, per_indiv_rate=1.0,
                            longest_result=None,
                            one_child=True, two_children=False):
        '''Performs multi-point crossover by selecting a point in each
        individual and exchanging the sequence of genes to the right
        (including the selection). The selected points are not
        necessarily the same in each individual.
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          longest_result : int [optional]
            The longest new individual to create. The crossover points
            are deliberately selected to avoid creating individuals
            longer than this. If there is no way to avoid creating a
            longer individual, the original individuals are returned and
            an ``'aborted'`` notification is sent to the monitor from
            ``'crossover_different'``.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation. `two_children` is the default.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is. If neither `one_child`
            nor `two_children` are specified, `two_children` is the
            default.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert longest_result is not True, "longest_result has no value"
        
        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or points < 1:
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration
        
        do_all_pairs = (per_pair_rate >= 1.0)
        points = int(points)
        longest_result = int(longest_result or 0)
        
        frand = rand.random
        shuffle = rand.shuffle
        
        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)
                
                if i1_len > points and i2_len > points:
                    i1_cuts = list(xrange(1, i1_len))
                    i2_cuts = list(xrange(1, i2_len))
                    shuffle(i1_cuts)
                    shuffle(i2_cuts)
                    i1_cuts = list(sorted(islice(i1_cuts, points)))
                    i2_cuts = list(sorted(islice(i2_cuts, points)))
                    i1_cuts.append(i1_len)
                    i2_cuts.append(i2_len)
                    
                    new_genes1 = list(i1_genome)
                    new_genes2 = list(i2_genome)
                    
                    for (i1_cut_i, i1_cut_j), (i2_cut_i, i2_cut_j) in \
                        izip(utils.pairs(iter(i1_cuts)), utils.pairs(iter(i2_cuts))):
                        
                        i1_cut = islice(new_genes1, i1_cut_i, i1_cut_j)
                        i2_cut = islice(new_genes2, i2_cut_i, i2_cut_j)
                        new_genes1 = list(chain(islice(new_genes1, i1_cut_i),
                                                i2_cut,
                                                islice(new_genes1, i1_cut_j, None)))
                        new_genes2 = list(chain(islice(new_genes2, i2_cut_i),
                                                i1_cut,
                                                islice(new_genes2, i2_cut_j, None)))
                    
                    i1_len, i2_len = len(new_genes1), len(new_genes2)
                    if longest_result and i1_len > longest_result:
                        notify('crossover_different', 'aborted',
                               { 'longest_result': longest_result, 'i1_len': i1_len })
                    else:
                        i1 = type(i1)(new_genes1, i1, statistic={ 'recombined': 1 })
                    
                    if longest_result and i2_len > longest_result:
                        notify('crossover_different', 'aborted',
                               { 'longest_result': longest_result, 'i2_len': i2_len })
                    else:
                        i2 = type(i2)(new_genes2, i2, statistic={ 'recombined': 1 })
            
            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2
    
    def crossover_one_different(self, _source,
                                per_pair_rate=None, per_indiv_rate=1.0,
                                longest_result=None,
                                one_child=False, two_children=False):
        '''A specialisation of `crossover_different` for single-point
        crossover.
        '''
        return self.crossover_different(
            _source,
            points=1,
            per_pair_rate=per_pair_rate, per_indiv_rate=per_indiv_rate,
            longest_result=longest_result,
            one_child=one_child, two_children=two_children)
    
    def crossover_two_different(self, _source,
                                per_pair_rate=None, per_indiv_rate=1.0,
                                longest_result=None,
                                one_child=False, two_children=False):
        '''A specialisation of `crossover_different` for two-point
        crossover.
        '''
        return self.crossover_different(
            _source,
            points=2,
            per_pair_rate=per_pair_rate, per_indiv_rate=per_indiv_rate,
            longest_result=longest_result,
            one_child=one_child, two_children=two_children)
    
    def crossover_segmented(self, _source,
                            per_pair_rate=None, per_indiv_rate=1.0, switch_rate=0.1,
                            one_child=False, two_children=False):   #pylint: disable=W0613
        '''Performs segmented crossover by exchanging random segments
        between two individuals. The first segment has `switch_rate`
        probability of being exchanged, while subsequent segments
        alternate between exchanging and non-exchanging.
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          switch_rate : |prob|
            The probability of the current segment ending. Exchanged
            segments are always followed by non-exchanged segments.
            
            This is also the probability of the first segment being
            exchanged. It is reset for each pair of individuals.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation. `two_children` is the default.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is. If neither `one_child`
            nor `two_children` are specified, `two_children` is the
            default.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert switch_rate is not True, "switch_rate has no value"
        
        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or not (0.0 < switch_rate < 1.0):
            if one_child:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration
        
        do_all_pairs = (per_pair_rate >= 1.0)
        
        frand = rand.random
        
        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)
                
                new_genes1 = list(i1_genome)
                new_genes2 = list(i2_genome)
                exchanging = (frand() < switch_rate)
                
                for i in xrange(i1_len if i1_len < i2_len else i2_len):
                    if exchanging:
                        new_genes1[i] = i2_genome[i]
                        new_genes2[i] = i1_genome[i]
                    if frand() < switch_rate:
                        exchanging = not exchanging
                
                i1 = type(i1)(new_genes1, i1, statistic={ 'recombined': 1 })
                i2 = type(i2)(new_genes2, i2, statistic={ 'recombined': 1 })
            
            if one_child:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2
コード例 #22
0
class Species(object):
    '''Abstract base class for species descriptors.
    '''

    _include_automatically = True
    '''Indicates whether the class should be included in the set of
    available species. If ``True``, the species is instantiated for
    every system and the contents of `public_context` is merged into the
    system context.
    
    This only applies to classes deriving from `Species` *and* included
    in `esec.species`. Other species classes are never included
    automatically.
    '''

    name = 'N/A'
    '''The display name of the species class.
    '''
    def __init__(self, cfg, eval_default):
        '''Initialises a new `Species` instance.
        
        :Parameters:
          cfg : dict, `ConfigDict`
            The set of configuration options applying to this species.
            No syntax is provided by the `Species` base class, but
            derived classes may require certain parameters.
          
          eval_default : evaluator
            The default evaluator for `Individual` instances of this
            species. Evaluators provide a method `eval` taking a single
            individual as a parameter.
        '''
        # Merge syntax and default details
        self.syntax = utils.merge_cls_dicts(self, 'syntax')
        self.cfg = ConfigDict(utils.merge_cls_dicts(self, 'default'))
        # Now apply user cfg details and test against syntax
        self.cfg.overlay(cfg)
        utils.cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
        # Store default evaluator
        self._eval_default = eval_default
        '''The default evaluator for individuals of this species type.'''
        # Initialise public_context if necessary
        if not hasattr(self, 'public_context'):
            self.public_context = {}
            '''Items to include in a system's execution context. This
            typically contains references to the initialisers provided
            by the species.'''

        # Set some default properties to imitiate Individual
        self.species = self
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual.species
        '''
        self._eval = eval_default
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual._eval
        '''
        self.statistic = {}
        '''Provided to make `Species` and `Individual` trivially
        compatible.
        
        :see: Individual.statistic
        '''

    def legal(self, indiv):  #pylint: disable=W0613,R0201
        '''Determines whether the specified individual is legal.
        
        By default, this function always returns ``True``. Subclasses
        may override this to perform range or bounds checking or other
        verification appropriate to the species.
        
        :See: esec.individual.Individual.legal
        '''
        return True

    #pylint: disable=R0201
    def mutate_insert(self,
                      _source,
                      per_indiv_rate=1.0,
                      length=None,
                      shortest=1,
                      longest=10,
                      longest_result=None):
        '''Mutates a group of individuals by inserting random gene
        sequences.
        
        Gene sequences are created by using the ``init_random`` method
        provided by the derived species type. This ``init_random``
        method must include a parameter named ``template`` which
        receives an individual to obtain bounds and values from.
        
        This method should be overridden for species that don't support
        random insertion directly into the genome.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken one at a
            time from this sequence and either returned unaltered or
            cloned and mutated.
          
          per_indiv_rate : |prob|
            The probability of any individual being mutated. If an
            individual is not mutated, it is returned unmodified.
          
          length : int > 0 [optional]
            The number of genes to insert at each mutation. If left
            unspecified, a random number between `shortest` and
            `longest` (inclusive) is used to determine the length.
          
          shortest : int > 0
            The smallest number of genes that may be inserted at any
            mutation.
          
          longest : int > `shortest`
            The largest number of genes that may be inserted at any
            mutation.
          
          longest_result : int > 0 [optional]
            The longest new genome that may be created. The length of
            the inserted segment is deliberately selected to avoid
            creating genomes longer than this. If there is no way to
            avoid creating a longer genome, the original individual
            is returned and an ``'aborted'`` notification is sent to
            the monitor from ``'mutate_insert'``.
        '''
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert length is not True, "length has no value"
        assert shortest is not True, "shortest has no value"
        assert longest is not True, "longest has no value"
        assert longest_result is not True, "longest_result has no value"

        if length is not None: shortest = longest = length

        shortest = int(shortest)
        longest = int(longest)
        longest_result = int(longest_result or maxsize)

        assert longest >= shortest, \
               "Value of longest (%d) must be higher or equal to shortest (%d)" % (longest, shortest)

        frand = rand.random
        irand = rand.randrange

        do_all_indiv = (per_indiv_rate >= 1.0)

        for indiv in _source:
            if do_all_indiv or frand() < per_indiv_rate:
                len_indiv = len(indiv.genome)
                cut = irand(len_indiv)
                lmax = (longest) if (len_indiv + longest < longest_result
                                     ) else (longest_result - len_indiv)
                indrand = indiv.init_random(length=longest, template=indiv)
                if lmax >= shortest:
                    insert = next(indrand)[:irand(shortest, lmax + 1)]
                    stats = {'mutated': 1, 'inserted_genes': len(insert)}
                    yield type(indiv)(indiv.genome[:cut] + insert +
                                      indiv.genome[cut:],
                                      indiv,
                                      statistic=stats)
                else:
                    value = {'i': indiv, 'longest_result': longest_result}
                    notify('mutate_insert', 'aborted', value)
                    yield indiv
            else:
                yield indiv

    #pylint: disable=R0201
    def mutate_delete(self,
                      _source,
                      per_indiv_rate=1.0,
                      length=None,
                      shortest=1,
                      longest=10,
                      shortest_result=1):
        '''Mutates a group of individuals by deleting random gene
        sequences.
        
        The number of genes to delete is selected randomly. If this
        value is the same as the number of genes in the individual, all
        but the first `shortest_result` genes are deleted.
        
        This method should be overridden for species that don't support
        random deletion directly from the ``genome`` property.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken one at a
            time from this sequence and either returned unaltered or
            cloned and mutated.
          
          per_indiv_rate : |prob|
            The probability of any individual being mutated. If an
            individual is not mutated, it is returned unmodified.
          
          length : int > 0 [optional]
            The number of genes to delete at each mutation. If left
            unspecified, a random number between `shortest` and
            `longest` (inclusive) is used to determine the length.
          
          shortest : int > 0
            The smallest number of genes that may be deleted at any
            mutation.
          
          longest : int > `shortest`
            The largest number of genes that may be deleted at any
            mutation.
          
          shortest_result : int > 0
            The shortest new genome that may be created. The length
            of the deleted segment is deliberately selected to avoid
            creating genomes shorter than this. If the original
            individual is this length or shorter, it is returned
            unmodified.
        '''
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert length is not True, "length has no value"
        assert shortest is not True, "shortest has no value"
        assert longest is not True, "longest has no value"
        assert shortest_result is not True, "shortest_result has no value"

        if length is not None: shortest = longest = length

        shortest = int(shortest)
        longest = int(longest)
        shortest_result = int(shortest_result)

        assert longest >= shortest, \
               "Value of longest (%d) must be higher or equal to shortest (%d)" % (longest, shortest)

        frand = rand.random
        irand = rand.randrange

        do_all_indiv = (per_indiv_rate >= 1.0)

        for indiv in _source:
            len_indiv = len(indiv.genome)
            if len_indiv > shortest_result and (do_all_indiv
                                                or frand() < per_indiv_rate):
                lmax = len_indiv - shortest_result
                if lmax > longest: lmax = longest
                length = irand(shortest, lmax +
                               1) if lmax >= shortest else len_indiv
                if length < len_indiv:
                    cut1 = irand(len_indiv - length)
                    cut2 = cut1 + length
                    stats = {'mutated': 1, 'deleted_genes': length}
                    yield type(indiv)(indiv.genome[:cut1] +
                                      indiv.genome[cut2:],
                                      indiv,
                                      statistic=stats)
                else:
                    new_indiv = indiv.genome[:shortest_result]
                    deleted = len_indiv - len(new_indiv)
                    if deleted:
                        yield type(indiv)(new_indiv,
                                          indiv,
                                          statistic={
                                              'mutated': 1,
                                              'deleted_genes': deleted
                                          })
                    else:
                        yield indiv
            else:
                yield indiv

    def crossover_uniform(self,
                          _source,
                          per_pair_rate=None,
                          per_indiv_rate=1.0,
                          per_gene_rate=0.5,
                          genes=None,
                          discrete=False,
                          one_child=True,
                          two_children=False):
        '''Performs uniform crossover by selecting genes at random from
        one of two individuals.
        
        Returns a sequence of crossed individuals based on the individuals
        in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a time
            from this sequence, recombined to produce two new individuals,
            and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          per_gene_rate : |prob|
            The probability of any particular pair of genes being swapped.
          
          discrete : bool
            If ``True``, uses discrete recombination, where source genes
            may be copied rather than exchanged, resulting in the same
            value appearing in both offspring.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert per_gene_rate is not True, "per_gene_rate has no value"
        assert genes is not True, "genes has no value"

        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or (per_gene_rate <= 0.0 and not genes):
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration

        do_all_pairs = (per_pair_rate >= 1.0)
        do_all_genes = (per_gene_rate >= 1.0)

        frand = rand.random

        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)

                new_genes1 = list(i1_genome)
                new_genes2 = list(i2_genome)
                source = xrange(i1_len if i1_len < i2_len else i2_len)

                if genes:
                    do_all_genes = True
                    source = list(source)
                    rand.shuffle(source)
                    source = islice(source, genes)

                for i in source:
                    if do_all_genes or frand() < per_gene_rate:
                        if discrete:
                            new_genes1[i] = i1_genome[i] if frand(
                            ) < 0.5 else i2_genome[i]
                            new_genes2[i] = i1_genome[i] if frand(
                            ) < 0.5 else i2_genome[i]
                        else:
                            new_genes1[i] = i2_genome[i]
                            new_genes2[i] = i1_genome[i]

                i1 = type(i1)(new_genes1, i1, statistic={'recombined': 1})
                i2 = type(i2)(new_genes2, i2, statistic={'recombined': 1})

            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2

    def crossover_discrete(self,
                           _source,
                           per_pair_rate=None,
                           per_indiv_rate=1.0,
                           per_gene_rate=1.0,
                           one_child=True,
                           two_children=False):
        '''A specialisation of `crossover_uniform` for discrete
        crossover.
        
        Note that `crossover_discrete` has a different default value for
        `per_gene_rate` to `crossover_uniform`.
        '''
        return self.crossover_uniform(_source=_source,
                                      per_pair_rate=per_pair_rate,
                                      per_indiv_rate=per_indiv_rate,
                                      per_gene_rate=per_gene_rate,
                                      discrete=True,
                                      one_child=one_child,
                                      two_children=two_children)

    def crossover(self,
                  _source,
                  points=1,
                  per_pair_rate=None,
                  per_indiv_rate=1.0,
                  one_child=True,
                  two_children=False):
        '''Performs crossover by selecting a `points` points common to
        both individuals and exchanging the sequences of genes to the
        right (including the selection).
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          points : int |ge| 1
            The number of points to cross at. If zero, individuals are
            returned unmodified (respecting the setting of
            `one_child`/`two_children`). If greater than the length of
            the individual, every gene will be exchanged.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is.
        '''
        assert points is not True, "points has no value"
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"

        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or points < 1:
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration

        do_all_pairs = (per_pair_rate >= 1.0)
        points = int(points)

        frand = rand.random
        shuffle = rand.shuffle

        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)

                if i1_len > points and i2_len > points:
                    max_len = i1_len if i1_len < i2_len else i2_len
                    cuts = list(xrange(1, max_len))
                    shuffle(cuts)
                    cuts = list(sorted(islice(cuts, points)))
                    cuts.append(max_len)

                    new_genes1 = list(i1_genome)
                    new_genes2 = list(i2_genome)

                    for cut_i, cut_j in utils.pairs(iter(cuts)):
                        cut1 = islice(new_genes1, cut_i, cut_j)
                        cut2 = islice(new_genes2, cut_i, cut_j)
                        new_genes1 = list(
                            chain(islice(new_genes1, cut_i), cut2,
                                  islice(new_genes1, cut_j, None)))
                        new_genes2 = list(
                            chain(islice(new_genes2, cut_i), cut1,
                                  islice(new_genes2, cut_j, None)))

                    i1 = type(i1)(new_genes1, i1, statistic={'recombined': 1})
                    i2 = type(i2)(new_genes2, i2, statistic={'recombined': 1})
            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2

    def crossover_one(self,
                      _source,
                      per_pair_rate=None,
                      per_indiv_rate=1.0,
                      one_child=True,
                      two_children=False):
        '''A specialisation of `crossover` for single-point crossover.
        '''
        return self.crossover(_source,
                              points=1,
                              per_pair_rate=per_pair_rate,
                              per_indiv_rate=per_indiv_rate,
                              one_child=one_child,
                              two_children=two_children)

    def crossover_two(self,
                      _source,
                      per_pair_rate=None,
                      per_indiv_rate=1.0,
                      one_child=True,
                      two_children=False):
        '''A specialisation of `crossover` for two-point crossover.'''
        return self.crossover(_source,
                              points=2,
                              per_pair_rate=per_pair_rate,
                              per_indiv_rate=per_indiv_rate,
                              one_child=one_child,
                              two_children=two_children)

    def crossover_different(
            self,
            _source,  #pylint: disable=R0915
            points=1,
            per_pair_rate=None,
            per_indiv_rate=1.0,
            longest_result=None,
            one_child=True,
            two_children=False):
        '''Performs multi-point crossover by selecting a point in each
        individual and exchanging the sequence of genes to the right
        (including the selection). The selected points are not
        necessarily the same in each individual.
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          longest_result : int [optional]
            The longest new individual to create. The crossover points
            are deliberately selected to avoid creating individuals
            longer than this. If there is no way to avoid creating a
            longer individual, the original individuals are returned and
            an ``'aborted'`` notification is sent to the monitor from
            ``'crossover_different'``.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation. `two_children` is the default.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is. If neither `one_child`
            nor `two_children` are specified, `two_children` is the
            default.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert longest_result is not True, "longest_result has no value"

        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or points < 1:
            if one_child and not two_children:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration

        do_all_pairs = (per_pair_rate >= 1.0)
        points = int(points)
        longest_result = int(longest_result or 0)

        frand = rand.random
        shuffle = rand.shuffle

        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)

                if i1_len > points and i2_len > points:
                    i1_cuts = list(xrange(1, i1_len))
                    i2_cuts = list(xrange(1, i2_len))
                    shuffle(i1_cuts)
                    shuffle(i2_cuts)
                    i1_cuts = list(sorted(islice(i1_cuts, points)))
                    i2_cuts = list(sorted(islice(i2_cuts, points)))
                    i1_cuts.append(i1_len)
                    i2_cuts.append(i2_len)

                    new_genes1 = list(i1_genome)
                    new_genes2 = list(i2_genome)

                    for (i1_cut_i, i1_cut_j), (i2_cut_i, i2_cut_j) in \
                        izip(utils.pairs(iter(i1_cuts)), utils.pairs(iter(i2_cuts))):

                        i1_cut = islice(new_genes1, i1_cut_i, i1_cut_j)
                        i2_cut = islice(new_genes2, i2_cut_i, i2_cut_j)
                        new_genes1 = list(
                            chain(islice(new_genes1, i1_cut_i), i2_cut,
                                  islice(new_genes1, i1_cut_j, None)))
                        new_genes2 = list(
                            chain(islice(new_genes2, i2_cut_i), i1_cut,
                                  islice(new_genes2, i2_cut_j, None)))

                    i1_len, i2_len = len(new_genes1), len(new_genes2)
                    if longest_result and i1_len > longest_result:
                        notify('crossover_different', 'aborted', {
                            'longest_result': longest_result,
                            'i1_len': i1_len
                        })
                    else:
                        i1 = type(i1)(new_genes1,
                                      i1,
                                      statistic={
                                          'recombined': 1
                                      })

                    if longest_result and i2_len > longest_result:
                        notify('crossover_different', 'aborted', {
                            'longest_result': longest_result,
                            'i2_len': i2_len
                        })
                    else:
                        i2 = type(i2)(new_genes2,
                                      i2,
                                      statistic={
                                          'recombined': 1
                                      })

            if one_child and not two_children:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2

    def crossover_one_different(self,
                                _source,
                                per_pair_rate=None,
                                per_indiv_rate=1.0,
                                longest_result=None,
                                one_child=False,
                                two_children=False):
        '''A specialisation of `crossover_different` for single-point
        crossover.
        '''
        return self.crossover_different(_source,
                                        points=1,
                                        per_pair_rate=per_pair_rate,
                                        per_indiv_rate=per_indiv_rate,
                                        longest_result=longest_result,
                                        one_child=one_child,
                                        two_children=two_children)

    def crossover_two_different(self,
                                _source,
                                per_pair_rate=None,
                                per_indiv_rate=1.0,
                                longest_result=None,
                                one_child=False,
                                two_children=False):
        '''A specialisation of `crossover_different` for two-point
        crossover.
        '''
        return self.crossover_different(_source,
                                        points=2,
                                        per_pair_rate=per_pair_rate,
                                        per_indiv_rate=per_indiv_rate,
                                        longest_result=longest_result,
                                        one_child=one_child,
                                        two_children=two_children)

    def crossover_segmented(self,
                            _source,
                            per_pair_rate=None,
                            per_indiv_rate=1.0,
                            switch_rate=0.1,
                            one_child=False,
                            two_children=False):  #pylint: disable=W0613
        '''Performs segmented crossover by exchanging random segments
        between two individuals. The first segment has `switch_rate`
        probability of being exchanged, while subsequent segments
        alternate between exchanging and non-exchanging.
        
        Returns a sequence of crossed individuals based on the
        individuals in `_source`.
        
        If `one_child` is ``True`` the number of individuals returned is
        half the number of individuals in `_source`, rounded towards
        zero. Otherwise, the number of individuals returned is the
        largest even number less than or equal to the number of
        individuals in `_source`.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          _source : iterable(`Individual`)
            A sequence of individuals. Individuals are taken two at a
            time from this sequence, recombined to produce two new
            individuals, and yielded separately.
          
          per_pair_rate : |prob|
            The probability of any particular pair of individuals being
            recombined. If two individuals are not recombined, they are
            returned unmodified. If this is ``None``, the value of
            `per_indiv_rate` is used.
          
          per_indiv_rate : |prob|
            A synonym for `per_pair_rate`.
          
          switch_rate : |prob|
            The probability of the current segment ending. Exchanged
            segments are always followed by non-exchanged segments.
            
            This is also the probability of the first segment being
            exchanged. It is reset for each pair of individuals.
          
          one_child : bool
            If ``True``, only one child is returned from each crossover
            operation. `two_children` is the default.
          
          two_children : bool
            If ``True``, both children are returned from each crossover
            operation. If ``False``, only one is. If neither `one_child`
            nor `two_children` are specified, `two_children` is the
            default.
        '''
        assert per_pair_rate is not True, "per_pair_rate has no value"
        assert per_indiv_rate is not True, "per_indiv_rate has no value"
        assert switch_rate is not True, "switch_rate has no value"

        if per_pair_rate is None: per_pair_rate = per_indiv_rate
        if per_pair_rate <= 0.0 or not (0.0 < switch_rate < 1.0):
            if one_child:
                skip = True
                for indiv in _source:
                    if not skip: yield indiv
                    skip = not skip
            else:
                for indiv in _source:
                    yield indiv
            raise StopIteration

        do_all_pairs = (per_pair_rate >= 1.0)

        frand = rand.random

        for i1, i2 in utils.pairs(_source):
            if do_all_pairs or frand() < per_pair_rate:
                i1_genome, i2_genome = i1.genome, i2.genome
                i1_len, i2_len = len(i1_genome), len(i2_genome)

                new_genes1 = list(i1_genome)
                new_genes2 = list(i2_genome)
                exchanging = (frand() < switch_rate)

                for i in xrange(i1_len if i1_len < i2_len else i2_len):
                    if exchanging:
                        new_genes1[i] = i2_genome[i]
                        new_genes2[i] = i1_genome[i]
                    if frand() < switch_rate:
                        exchanging = not exchanging

                i1 = type(i1)(new_genes1, i1, statistic={'recombined': 1})
                i2 = type(i2)(new_genes2, i2, statistic={'recombined': 1})

            if one_child:
                yield i1 if frand() < 0.5 else i2
            else:
                yield i1
                yield i2
コード例 #23
0
    def __init__(self, cfg, lscape=None, monitor=None):
        # Merge syntax and default details
        self.syntax = merge_cls_dicts(self, 'syntax')
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
        # Merge in all globally defined ESDL functions
        for key, value in GLOBAL_ESDL_FUNCTIONS.iteritems():
            self.cfg.system[key.lower()] = value
        # Now apply user cfg details and test against syntax
        self.cfg.overlay(cfg)
        # If no default evaluator has been provided, use `lscape`
        cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
        
        # Initialise empty members
        self._code = None
        self._code_string = None
        
        self.monitor = None
        self.selector = None
        self.selector_current = None
        
        self._in_step = False
        self._next_block = []
        self._block_cache = {}

        # Compile code
        self.definition = self.cfg.system.definition
        self._context = context = {
            'config': self.cfg,
            'rand': random.Random(cfg.random_seed),
            'notify': self._do_notify
        }
        
        # Add species settings to context
        for cls in SPECIES:
            inst = context[cls.name] = cls(self.cfg, lscape)
            try:
                for key, value in inst.public_context.iteritems():
                    context[key.lower()] = value
            except AttributeError: pass

        # Add external values to context
        for key, value in self.cfg.system.iteritems():
            if isinstance(key, str):
                key_lower = key.lower()
                if key_lower in context:
                    warn("Overriding variable/function '%s'" % key_lower)
                context[key_lower] = value
            else:
                warn('System dictionary contains non-string key %r' % key)
        
        
        model, self.validation_result = compileESDL(self.definition, context)
        if not self.validation_result:
            raise ESDLCompilerError(self.validation_result, "Errors occurred while compiling system.")
        self._code_string, internal_context = emit(model, out=None, optimise_level=0, profile='_profiler' in context)
        
        internal_context['_yield'] = lambda name, group: self.monitor.on_yield(self, name, group)
        internal_context['_alias'] = GroupAlias
        
        for key, value in internal_context.iteritems():
            if key in context:
                warn("Variable/function '%s' is overridden by internal value" % key)
            context[key] = value

        esec.context._context.context = context
        esec.context._context.config = context['config']
        esec.context._context.rand = context['rand']
        esec.context._context.notify = context['notify']
        
        self.monitor = monitor or MonitorBase()
        self.selector = self.cfg['selector'] or [name for name in model.block_names if name != model.INIT_BLOCK_NAME]
        self.selector_current = iter(self.selector)
        
        for func in model.externals.iterkeys():
            if func not in context:
                context[func] = OnIndividual(func)
        
        self._code = compile(self._code_string, 'ESDL Definition', 'exec')
コード例 #24
0
class System(object):
    '''Provides a system using a dynamically generated controller.
    '''
    
    syntax = {
        'system': {
            # The textual description of the system using ESDL
            'definition': str,
        },
        # The block selector (must support iter(selector))
        'selector?': '*'
    }
    
    default = {
        'system': {
            # Filters are specified using esdl_func for unbound
            # functions or public_context when bound to a species.
            # All other filters are assumed to be OnIndividual and are
            # included implicitly.
        }
    }
    
    
    def __init__(self, cfg, lscape=None, monitor=None):
        # Merge syntax and default details
        self.syntax = merge_cls_dicts(self, 'syntax')
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
        # Merge in all globally defined ESDL functions
        for key, value in GLOBAL_ESDL_FUNCTIONS.iteritems():
            self.cfg.system[key.lower()] = value
        # Now apply user cfg details and test against syntax
        self.cfg.overlay(cfg)
        # If no default evaluator has been provided, use `lscape`
        cfg_validate(self.cfg, self.syntax, type(self), warnings=False)
        
        # Initialise empty members
        self._code = None
        self._code_string = None
        
        self.monitor = None
        self.selector = None
        self.selector_current = None
        
        self._in_step = False
        self._next_block = []
        self._block_cache = {}

        # Compile code
        self.definition = self.cfg.system.definition
        self._context = context = {
            'config': self.cfg,
            'rand': random.Random(cfg.random_seed),
            'notify': self._do_notify
        }
        
        # Add species settings to context
        for cls in SPECIES:
            inst = context[cls.name] = cls(self.cfg, lscape)
            try:
                for key, value in inst.public_context.iteritems():
                    context[key.lower()] = value
            except AttributeError: pass

        # Add external values to context
        for key, value in self.cfg.system.iteritems():
            if isinstance(key, str):
                key_lower = key.lower()
                if key_lower in context:
                    warn("Overriding variable/function '%s'" % key_lower)
                context[key_lower] = value
            else:
                warn('System dictionary contains non-string key %r' % key)
        
        
        model, self.validation_result = compileESDL(self.definition, context)
        if not self.validation_result:
            raise ESDLCompilerError(self.validation_result, "Errors occurred while compiling system.")
        self._code_string, internal_context = emit(model, out=None, optimise_level=0, profile='_profiler' in context)
        
        internal_context['_yield'] = lambda name, group: self.monitor.on_yield(self, name, group)
        internal_context['_alias'] = GroupAlias
        
        for key, value in internal_context.iteritems():
            if key in context:
                warn("Variable/function '%s' is overridden by internal value" % key)
            context[key] = value

        esec.context._context.context = context
        esec.context._context.config = context['config']
        esec.context._context.rand = context['rand']
        esec.context._context.notify = context['notify']
        
        self.monitor = monitor or MonitorBase()
        self.selector = self.cfg['selector'] or [name for name in model.block_names if name != model.INIT_BLOCK_NAME]
        self.selector_current = iter(self.selector)
        
        for func in model.externals.iterkeys():
            if func not in context:
                context[func] = OnIndividual(func)
        
        self._code = compile(self._code_string, 'ESDL Definition', 'exec')
    
    def _do_notify(self, sender, name, value):
        '''Queues a message for the current monitor.
        
        The message consists of a string `name` and an object `value`.
        The sender is either a string or an object reference identifying
        the source of the message.
        
        For example, a mutation operator may include::
            
            notify("mutate_random", "population", 10)
        
        to indicate that ten members of ``population`` were mutated.
        '''
        self.monitor._on_notify(sender, name, value)    #pylint: disable=W0212
    
    def info(self, level):
        '''Report the current configuration.
        
        .. include:: epydoc_include.txt
        
        :Parameters:
          level : int |ge| 0
            The verbosity level.
        '''
        result = []
        if level > 0:
            result.append('>> System Definition:')
            result.append(self.definition.strip(' \t').strip('\n'))
            result.append('')
        if level > 3:
            result.append('>> Compiled Code:')
            result.append(self._code_string)
            result.append('')
        if level > 2:
            result.append('>> Experiment Configuration:')
            result.extend(self.cfg.list())
        if level > 4:
            result.append('>> System context:')
            result.extend(ConfigDict(self._context).list())
        return result
    
    def seed_offset(self, offset):
        '''Re-seed the random instance (shared everywhere) with an
        offset amount. Can be called before each ``run()`` for
        repeatable variation.
        '''
        seed = self.cfg.random_seed
        self._context['rand'].seed(seed + offset)
        print '>> New Seed: %d + %d (offset)' % (seed, offset)
    
    def begin(self):
        '''Begins the system. Each call to `step` executes one
        generation.
        '''
        # Allowed to use exec
        #pylint: disable=W0122
        
        try:
            self.monitor.on_run_start(self)
            self.monitor.on_pre_reset(self)
            
            # Reset the birthday counter
            Individual.reset_birthday()
            
            # Reset the block invocation cache
            self._block_cache = { }
            
            # Reset externally specified variables
            inner_context = self._context
            for key in self.cfg.system.iterkeys():
                key_lower = key.lower()
                if key_lower in inner_context:
                    inner_context[key_lower] = self.cfg.system[key]
            
            # Run the initialisation block
            exec self._code in self._context
            
            self.monitor.on_post_reset(self)
        except KeyboardInterrupt:
            raise
        except:
            ex = sys.exc_info()
            if ex[0] is EvaluatorError:
                ex_type, ex_value, ex_trace = ex[1].args
            else:
                ex_type, ex_value = ex[0], ex[1]
                ex_trace = ''.join(traceback.format_exception(*ex))
            self.monitor.on_exception(self, ex_type, ex_value, ex_trace)
            self.monitor.on_post_reset(self)
            self.monitor.on_run_end(self)
            return
    
    def step(self, block=None):
        '''Executes one or more iteration. If `step` is called from the
        monitor's ``on_pre_breed``, ``on_post_breed`` or
        ``on_exception``, more than one iteration will occur. Otherwise,
        only one iteration will be executed.
        
        :Parameters:
          block : string [optional]
            The name of the block to execute. If specified, the block is
            used for the first iteration executed. If omitted, the block
            selector associated with the system is queried for each
            iteration.
            
            This name is not case-sensitive: it is converted to
            lowercase before use.
        '''
        self._next_block.append(block)
        
        if self._in_step:
            # If step() has been called from one of our own callbacks,
            # we should return now and let the while loop below pick up
            # the next block.
            return
        
        try:
            self._in_step = True
            while self._next_block:
                # _next_block may be appended to by a callback
                block = self._next_block.pop(0)
                
                if block:
                    block_name = str(block).lower()
                elif isinstance(self.selector, list) and len(self.selector) == 1:
                    # Performance optimisation for default single block case
                    block_name = str(self.selector[0]).lower()
                else:
                    block_name = None
                
                try:
                    self.monitor.on_pre_breed(self)
                    
                    if block_name is None:
                        try:
                            block_name = next(self.selector_current)
                        except StopIteration:
                            self.selector_current = iter(self.selector)
                            block_name = next(self.selector_current)
                        block_name = str(block_name).lower()
                    
                    try:
                        codeobj = self._block_cache.get(block_name)
                        if codeobj is None:
                            codeobj = compile('_block_' + block_name + '()', 'Invoke ' + block_name, 'exec')
                            self._block_cache[block_name] = codeobj
                        exec codeobj in self._context   #pylint: disable=W0122
                        self.monitor.notify('System', 'Block', block_name)
                    except NameError:
                        if ('_block_' + block_name) not in self._context:
                            # This will be caught immediately and passed to the monitor
                            raise NameError('ESDL block %s is not defined' % block_name)
                        else:
                            raise
                
                except KeyboardInterrupt:
                    self.monitor.on_run_end(self)
                    raise
                except:
                    ex = sys.exc_info()
                    ex_type, ex_value = ex[0], ex[1]
                    ex_trace = ''.join(traceback.format_exception(*ex))
                    self.monitor.on_exception(self, ex_type, ex_value, ex_trace)
                
                self.monitor.on_post_breed(self)
        finally:
            self._in_step = False
    
    def close(self):
        '''Executes clean-up code.'''
        self.monitor.on_run_end(self)
コード例 #25
0
ファイル: run.py プロジェクト: flying-circus/esec
def esec_batch(options):
    '''Runs a batch file of configurations and saves the results.
    
    Results are saved to::
    
        ./results/batchname/<index>.* data files
    
    A summary file of the batch and associated tags is saved in::
    
        ./results/batchname/_tags.(txt|csv)
    
    A summary file of the summary line from each experiment is saved
    in::
    
        ./results/batchname/_summary.(txt|csv)
    
    Data is (depending on settings) saved or appended to files::
    
        ./results/batchname/<id>._config.txt        # full config details
        ./results/batchname/<id>._summary.(txt|csv) # appended run results
        ./results/batchname/<id>.<run>.(txt|csv)    # per gen step results
    
    Handy ``run.py`` settings (-s "...") for batch runs include::
        
    batch.dry_run=True
        Create each configuration to test that they work but do not
        execute them.
    
    batch.start_at=...
        Configuration id to start at. Will run the ``start_at`` id.
    
    batch.stop_at=...
        Configuration id to stop at. Will run the ``stop_at id``, but
        not after it.
    
    batch.include_tags=[...]
        Only run experiments that include these tags
    
    batch.exclude_tags=[...]
        Do not run experiments that include these tags
    
    batch.pathbase="..."
        Relative path to store results in
    
    batch.summary=True
        Create a summary file of all experiments
    
    batch.csv=True
        Create CSV files instead of TXT files (except for config)
    
    batch.low_priority=True
        Run with low CPU priority
    
    batch.quiet=True
        Hide console output
    
    '''
    # Disable pylint complaints about branches and local variables
    #pylint: disable=R0912,R0914
    
    options.batch, _, tag_names = options.batch.partition('+')
    # A batch file is a normal .py file with a method named "batch" that 
    # returns a sequence of tuples of settings.
    mod = _load_module('cfgs', options.batch)
    if not mod:
        raise ImportError('Cannot find ' + options.batch + ' as batch file.')
    # Update configs with anything specified in the batch file
    configs.update(mod.get('configs', None) or { })
    # Get any settings overrides from the batch file
    batch = mod.get('batch')()
    batch_settings = mod.get('settings', '')
    # Get config defaults (allows batch files to import plugins directly)
    batch_default = ConfigDict(default)
    batch_default.overlay(mod.get('defaults', { }))
    
    print '>>>>', batch_settings
    
    # Initialise batch settings
    batch_cfg = ConfigDict(batch_settings_default)
    batch_cfg.pathbase = os.path.join('results', options.batch)
    
    # Handle any extra settings
    batch_cfg.overlay(settings_split(batch_settings))
    
    for key, value in settings_split(options.settings).iteritems():
        # Only want batch settings, and don't want the "batch." prefix
        if key.startswith('batch.'):
            batch_cfg[key[6:]] = value
    
    # Ensure the batch syntax is valid. Exit if errors and warn on
    # keys that aren't in the syntax.
    errors, warnings, other_keys = batch_cfg.validate(batch_settings_syntax)
    if errors:
        print >> sys.stderr, "Batch configuration settings are invalid:"
        print >> sys.stderr, '  ' + '\n  '.join(str(ex) for ex in errors)
        return
    else:
        if warnings:
            print >> sys.stderr, "Batch configuration warnings:"
            print >> sys.stderr, '  ' + '\n  '.join(str(warning) for warning in warnings)
            print >> sys.stderr
        if other_keys:
            print >> sys.stderr, "Batch configuration contains unknown settings:"
            print >> sys.stderr, '  ' + '\n  '.join('%s: %r' % (key, batch_cfg[key]) for key in other_keys)
            print >> sys.stderr
    
    if isinstance(batch_cfg.include_tags, str): batch_cfg.include_tags = batch_cfg.include_tags.split('+')
    if isinstance(batch_cfg.exclude_tags, str): batch_cfg.exclude_tags = batch_cfg.exclude_tags.split('+')
    batch_cfg.include_tags = set(batch_cfg.include_tags or set())
    batch_cfg.exclude_tags = set(batch_cfg.exclude_tags or set())
    
    if tag_names:
        tag_names = tag_names.split('+')
        batch_cfg.include_tags.update(t for t in tag_names if t[0] != '!')
        batch_cfg.exclude_tags.update(t[1:] for t in tag_names if t[0] == '!')
    
    # Output file extension is '.txt' unless the csv setting is True.
    extension = '.txt'
    if batch_cfg.csv:
        extension = '.csv'
    
    # Lower the process priority if requested.
    if batch_cfg.low_priority:
        _set_low_priority()
    
    # Create a directory for results and warn if already exists
    pathbase = os.path.abspath(batch_cfg.pathbase)
    try:
        # makedirs creates the path recursively.
        os.makedirs(pathbase)
    except OSError:
        warn('Output folder already exists and may contain output from a previous run (%s)' % pathbase)
    
    # Is this a tag summary run?
    if batch_cfg.include_tags or batch_cfg.exclude_tags:
        # Generate a summary file of cfgids, tags and format strings
        tags_file = open(os.path.join(pathbase, '_tags' + extension), 'w')
        if batch_cfg.csv:
            tags_file.write("Id,Tags,Format\n")
        else:
            tags_file.write("# id, tags and format strings. \n")
        # Track tags and the ids that match them
        summary_tags = collections.defaultdict(list)
    
    # Create a super summary (summary of the summary lines)
    summary_file = open(os.path.join(pathbase, '_summary' + extension), 'w')
    
    # Run each configuration of the batch
    for i, batch_item in enumerate(batch):
        # Use get method (dictionary) if available;
        # otherwise, assume compatibility mode (tuple).
        if hasattr(batch_item, 'get'):
            tags = batch_item.get('tags', set([]))
            names = batch_item.get('names', None)
            config = batch_item.get('config', None)
            settings = batch_item.get('settings', None)
            fmt = batch_item.get('format', None) or batch_item.get('fmt', None)
        else:
            tags, names, config, settings, fmt = batch_item
        
        # Use cfgid instead of converting i repeatedly
        cfgid = '%04d' % i
        if batch_cfg.include_tags or batch_cfg.exclude_tags:
            if tags:
                # Write the summary of cfgid, tags and format string.
                if batch_cfg.csv:
                    tags_file.write('%d,"%s","%s"\n' % (i, tags, fmt))
                else:
                    tags_file.write("%s; %s; %s\n" % (cfgid, tags, fmt))
                # Track the cfgid against the tags listed (all printed later)
                for tag in tags:
                    summary_tags[tag].append(cfgid)
            else:
                # Write the summary of cfgid and format string.
                if batch_cfg.csv:
                    tags_file.write('%d,,"%s"\n' % (i, fmt))
                else:
                    tags_file.write("%s; ; %s\n" % (cfgid, fmt))
        # Start and stop limits?
        if i < batch_cfg.start_at: continue
        if i > batch_cfg.stop_at: break
        # Include/exclude list?
        if (batch_cfg.include_tags and not batch_cfg.include_tags.intersection(tags) or
            batch_cfg.exclude_tags and batch_cfg.exclude_tags.intersection(tags)):
            continue
        # Print an obvious header
        print '\n** ' + "*"*117
        print ' **'
        print ("  ** Experiment %04d." % i), (("Tags %s" % tags) if tags else "")
        print ' **'
        print "** "+ "*"*117 + '\n'
        
        # Overlay any configuration names specified for this run.
        if names:
            try:
                cfg = _load_config(names, batch_default)
            except AttributeError:
                print >> sys.stderr, 'Loading config file(s) failed: '+ names
                raise
        else:
            cfg = ConfigDict(batch_default)
        
        # Overlay any config dictionary (copy to avoid shared reference issues)
        cfg.overlay(ConfigDict(config) if isinstance(config, ConfigDict) else config)
        # Override cfg.verbose
        if options.verbose >= 0:
            cfg.verbose = int(options.verbose)
        # Use settings strings to override configurations
        for key, value in settings_split(settings).iteritems():
            cfg.set_by_name(key, value)
        
        # Write summary to a buffer first, then only include the second line
        # in the super summary file (ignore headings)
        summary_buffer = StringIO()

        # Helper function to open a unique file
        def _open(filepattern, mode='w'):
            '''Returns an open file. `filepattern` must contain a ``%d``
            value so a unique index may be included.
            '''
            i = 0
            filename = filepattern % i
            # Not reliable, but no other choice in Python
            # (specifically, open() has no way to fail when a file exists)
            while os.path.exists(filename):
                i += 1
                filename = filepattern % i
            return open(filename, mode)
        
        # Close files/objects in this list after this run
        open_files = []
        
        # If the monitor has been specified as a dictionary, specify output files.
        # If the monitor has been specified directly, don't try and change it.
        if isinstance(cfg.monitor, (ConfigDict, dict)):
            report_out = _open(os.path.join(pathbase, cfgid + '.%04d' + extension))
            summary_out = _open(os.path.join(pathbase, cfgid + '.%04d._summary' + extension))
            config_out = _open(os.path.join(pathbase, cfgid + '.%04d._config.txt'))
            open_files.extend((report_out, summary_out, config_out))
            
            if not batch_cfg.csv:
                if batch_cfg.quiet:
                    # MultiTarget sends the same output to both the console and the files.
                    cfg.overlay({'monitor': {
                        'report_out': report_out,
                        'summary_out': MultiTarget(summary_out, sys.stdout, summary_buffer),
                        'config_out': config_out,
                        'error_out': MultiTarget(summary_out, sys.stderr),
                        'verbose': max(4, cfg.verbose),
                    }})
                else:
                    # MultiTarget sends the same output to both the console and the files.
                    cfg.overlay({'monitor': {
                        'report_out': MultiTarget(report_out, sys.stdout),
                        'summary_out': MultiTarget(summary_out, sys.stdout, summary_buffer),
                        'config_out': MultiTarget(config_out, sys.stdout),
                        'error_out': MultiTarget(summary_out, sys.stderr),
                        'verbose': max(4, cfg.verbose),
                    }})
            else:
                # MultiMonitor sends the same callbacks to different monitors.
                monitor_cfg = ConfigDict(cfg.monitor)
                if batch_cfg.quiet:
                    monitor_cfg['report_out'] = None
                    monitor_cfg['config_out'] = None
                console_monitor = ConsoleMonitor(monitor_cfg)
                monitor_cfg.overlay({
                    'report_out': report_out,
                    'summary_out': MultiTarget(summary_out, summary_buffer),
                    'config_out': config_out,
                    'error_out': summary_out,
                    'verbose': max(4, cfg.verbose),
                })
                csv_monitor = CSVMonitor(monitor_cfg)
                
                cfg.monitor = {
                    'class': MultiMonitor,
                    'monitors': [ console_monitor, csv_monitor ]
                }
        
        # Create an Experiment instance
        try:
            ea_exp = Experiment(cfg)
        except:
            ea_exp = None
        
        # Run the application (and time it)
        if batch_cfg.dry_run:
            print '--> DRY RUN DONE <--'
        elif ea_exp:
            start_time = time.clock()
            ea_exp.run()
            print '->> DONE <<- in ', (time.clock() - start_time)
        else:
            print '--> ERRORS OCCURRED <--'
        
        # Write the summary to the super summary file.
        if summary_file and not batch_cfg.dry_run:
            summary_lines = summary_buffer.getvalue().splitlines()[:2]
            if len(summary_lines) != 2:
                summary_lines = ['-', '-']
            if batch_cfg.csv:
                if i == 0:
                    summary_file.write('#,' + summary_lines[0] + '\n')
                summary_file.write('%d,%s\n' % (i, summary_lines[1]))
            else:
                if i == 0:
                    summary_file.write('  #  ' + summary_lines[0] + '\n')
                summary_file.write('%04d %s\n' % (i, summary_lines[1]))
            summary_file.flush()
        
        for obj in open_files: obj.close()
        
    # Save the tag data
    if batch_cfg.include_tags or batch_cfg.exclude_tags:
        tags_file.write("#\n# Summary of cfgid's per tag\n#\n")
        if batch_cfg.csv:
            for tag in sorted(summary_tags.keys()):
                tags_file.write(tag+','+','.join(str(s) for s in summary_tags[tag])+'\n')
        else:
            for tag in sorted(summary_tags.keys()):
                tags_file.write(tag+'='+str(summary_tags[tag])+'\n')
        tags_file.close()
        print 'Tag Run done!'
コード例 #26
0
ファイル: __init__.py プロジェクト: flying-circus/esec
class MonitorBase(object):
    '''Defines the base class for monitors to be used with ESDL defined
    systems.
    
    `MonitorBase` can also be used as a 'do-nothing' monitor.
    
    While `MonitorBase` does not make use of it, configuration
    dictionaries are supported. The default initialiser accepts a
    configuration which it will overlay over child ``syntax`` and
    ``default`` dictionaries.
    '''

    syntax = {}
    default = {}

    def __init__(self, cfg=None):
        '''Performs configuration dictionary overlaying.'''
        self.syntax = merge_cls_dicts(self, 'syntax')  # all in hierarchy
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
        if cfg:
            self.cfg.overlay(cfg)  # apply user variables
        cfg_validate(self.cfg,
                     self.syntax,
                     type(self).__name__,
                     warnings=False)

    _required_methods = [
        'on_yield', 'on_notify', 'notify', 'on_pre_reset', 'on_post_reset',
        'on_pre_breed', 'on_post_breed', 'on_run_start', 'on_run_end',
        'on_exception', 'should_terminate'
    ]

    @classmethod
    def isinstance(cls, inst):
        '''Returns ``True`` if `inst` is compatible with `MonitorBase`.
        
        An object is considered compatible if it is a subclass of
        `MonitorBase`, or if it implements the same methods. Methods are
        not tested for signatures, which may result in errors occurring
        later in the program.
        '''
        if isinstance(inst, cls): return True
        if inst is None: return False
        return all(hasattr(inst, method) for method in cls._required_methods)

    def on_yield(self, sender, name, group):
        '''Called for each population YIELDed in the system.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_post_breed` will be called and if
        `should_terminate` returns ``False`` execution will continue
        normally.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_notify(self, sender, name, value):
        '''Called in response to notifications from other objects. For
        example, a mutation operation may call ``notify`` to report to
        the monitor how many individuals were mutated. The monitor
        receives this message through `on_notify` and either ignores it
        or retains the statistic.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_post_breed` will be called and if
        `should_terminate` returns ``False`` execution will continue
        normally.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the call
            to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        '''
        pass

    def _on_notify(self, sender, name, value):
        '''Handles notification messages.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the call
            to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        
        :Warn:
            Do not override this method to handle messages.
            That is what `on_notify` is for. Only override this
            method if you are implementing a queuing or
            synchronisation mechanism.
        '''
        self.on_notify(sender, name, value)

    def notify(self, sender, name, value):
        '''Sends a notification message to this monitor. This is used in
        contexts where a reference to the monitor is readily available.
        If the global ``notify`` function is available (for example, in
        selectors, generators or evaluators) it should be used instead.
        
        The global ``notify`` function can be obtained by importing
        `esec.context.notify`.
        
        :Parameters:
          sender
            The sender of the notification message as provided by the
            call to ``notify``.
          
          name : string
            The name of the notification message as provided by the
            call to ``notify``.
          
          value
            The value of the notification message as provided by the
            call to ``notify``.
        '''
        self._on_notify(sender, name, value)

    def on_pre_reset(self, sender):
        '''Called when the groups are reset, generally immediately after
        `on_run_start` is called.
        
        If this function or the system reset raises an exception, it
        will be passed to `on_exception`, `on_run_end` will be called
        and the run will be terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_post_reset(self, sender):
        '''Called immediately after the initialisation code specified in
        the system definition has executed.
        
        If this function or the system reset raises an exception, it
        will be passed to `on_exception`, `on_run_end` will be called
        and the run will be terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_pre_breed(self, sender):
        '''Called before breeding the current generation.
        
        If this function or the system breed raises an exception, it
        will be passed to `on_exception`, `on_post_breed` will be called
        and if `should_terminate` returns ``False`` execution will
        continue normally.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_post_breed(self, sender):
        '''Called after breeding the current generation.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_run_start(self, sender):
        '''Called at the beginning of a run, before `on_pre_reset`.
        
        If this function raises an exception, it will be passed to
        `on_exception`, `on_run_end` will be called and the run will be
        terminated.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_run_end(self, sender):
        '''Called at the end of a run, regardless of the reason for
        ending.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        '''
        pass

    def on_exception(self, sender, exception_type, value, trace):
        '''Called when an exception is thrown.
        
        If this function raises an exception, it will be handled by the
        Python interpreter.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
          
          exception_type : type(Exception)
            The type object representing the exception that was raised.
          
          value : Exception object
            The exception object that was raised.
          
          trace : string
            A displayable exception message formatted using the
            ``traceback`` module.
        '''
        pass

    def should_terminate(self, sender):  #pylint: disable=R0201,W0613
        '''Called after each experiment step to determine whether to
        terminate.
        
        :Parameters:
          sender : `esec.system.System`
            The system instance reporting to this monitor.
        
        :Returns:
            ``True`` if the run should terminate immediately; otherwise,
            ``False``.
        '''
        return True
コード例 #27
0
ファイル: __init__.py プロジェクト: flying-circus/esec
class Landscape(object):
    '''Abstract base class for parameterised evaluators.
    
    See `landscape` for information on how the `Landscape` class assists
    with evaluator implementation.

    See derived classes for more details, or the landscape testing code
    to see how ``test_key`` and ``test_cfg`` are used for.
    '''

    ltype = '--base--'  # problem type base classes should set this
    ltype_name = 'Underived'  # pretty name for landscape types
    lname = '--none--'  # problem type subclasses should overwrite this

    maximise = True  # is the default objective maximise? (ie fitness)
    size_equals_parameters = True  # should size.exact == parameters?
    syntax = { # configuration syntax key's and type. MERGED
        'class?': type, # specific class of landscape
        'instance?': '*', # landscape instance
        'random_seed': [int, None], # for random number instance
        'invert?': bool,
        'offset?': float,
        'parameters': [None, int],   # may be used by subclasses
        'size': {   # size should be used for all genome size references
            'min': int,
            'max': int,
            'exact': int
        },
    }
    default = { # default syntax values. MERGED
        'random_seed': None,
        'invert': False,
        'offset': 0.0,
        'parameters': None,
        'size': {
            'min': 0,
            'max': 0,
            'exact': 0
        },
    }

    test_key = ()  # test keys used for linear configuration strings
    test_cfg = ()  # tuple of linear configuration strings

    strict = {}  # eg. 'size.exact':2  <- NOT merged like syntax/default

    def __init__(self, cfg=None, **other_cfg):
        '''Initialises the landscape.
        
        This method should be overridden to perform any once-off setup
        that is required, such as generating a landscape map or sequence
        of test cases.
        
        :Warn:
            This initialiser is called *before* the system is
            constructed. Importantly, the members of `esec.context` have
            not been initialised and will raise exceptions if accessed.
        '''
        self.syntax = merge_cls_dicts(self, 'syntax')  # all in hierarchy
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))

        if cfg is None: cfg = ConfigDict()
        elif not isinstance(cfg, ConfigDict): cfg = ConfigDict(cfg)

        self.cfg.overlay(cfg)
        for key, value in other_cfg.iteritems():
            if key in self.syntax:
                self.cfg.set_by_name(key, value)
            elif key.partition('_')[0] in self.syntax:
                self.cfg.set_by_name(key.replace('_', '.'), value)

        cfg_validate(self.cfg, self.syntax, self.ltype + ':' + self.lname)

        # Initialise size properties
        self.size = self.cfg.size
        if self.size_equals_parameters and self.cfg.parameters:
            self.size.exact = self.cfg.parameters
        if self.size.exact: self.size.min = self.size.max = self.size.exact
        if self.size.min >= self.size.max:
            self.size.exact = self.size.max = self.size.min

        # Now check for any strict limits (ie parameters)
        cfg_strict_test(self.cfg, self.strict)

        # random seed?
        if not isinstance(self.cfg.random_seed, int):
            random.seed()
            self.cfg.random_seed = cfg.random_seed = random.randint(
                0, sys.maxint)
        self.rand = Random(self.cfg.random_seed)

        # inversion? offset?
        self.invert = self.cfg.invert
        self.offset = self.cfg.offset

        # Autobind _eval_minimise or _eval_maximise.
        if not hasattr(self, 'eval'):
            if self.maximise == self.invert:
                setattr(self, 'eval', self._eval_minimise)
            else:
                setattr(self, 'eval', self._eval_maximise)

    def _eval_maximise(self, indiv):
        '''Evaluates the provided individual and wraps the result in a
        `FitnessMaximise` object.
        '''
        fitness = self._eval(indiv)  #pylint: disable=E1101
        if isinstance(fitness, Fitness): return fitness
        else: return FitnessMaximise(fitness + self.offset)

    def _eval_minimise(self, indiv):
        '''Evaluates the provided individual and wraps the result in a
        `FitnessMinimise` object.
        '''
        fitness = self._eval(indiv)  #pylint: disable=E1101
        if isinstance(fitness, Fitness): return fitness
        else: return FitnessMinimise(fitness + self.offset)

    def legal(self, indiv):  #pylint: disable=W0613,R0201
        '''Determines whether the specified individual is legal.
        
        By default, this function always returns ``True``. Subclasses
        may override this to perform other verification appropriate to
        the landscape.
        
        :See: esec.individual.Individual.legal
        '''
        return True

    @classmethod
    def by_cfg_str(cls, cfg_str):
        '''Used by test framework to initialise a class instance using a
        simple test string specified in each class.test_cfg as nested
        tuples.
        
        :rtype: Landscape
        '''
        # create ConfigDict using cfg_str (defaults not needed but why not)
        cfg = ConfigDict()
        # map string to appropriate keys and types (or nested keys)
        cfg.set_linear(cls.test_key, cfg_str)
        # provide a new instance
        return cls(cfg)

    def info(self, level):
        '''Return landscape info for any landscape
        '''
        result = ['Using %s %s landscape' % (a_or_an(self.lname), self.lname)]
        if level > 3:
            result.append('')
            result.append('Configuration:')
            result.extend(self.cfg.lines())
        return result
コード例 #28
0
ファイル: __init__.py プロジェクト: flying-circus/esec
class Landscape(object):
    '''Abstract base class for parameterised evaluators.
    
    See `landscape` for information on how the `Landscape` class assists
    with evaluator implementation.

    See derived classes for more details, or the landscape testing code
    to see how ``test_key`` and ``test_cfg`` are used for.
    '''

    ltype = '--base--' # problem type base classes should set this
    ltype_name = 'Underived'    # pretty name for landscape types
    lname = '--none--' # problem type subclasses should overwrite this
    
    maximise = True # is the default objective maximise? (ie fitness)
    size_equals_parameters = True # should size.exact == parameters?
    syntax = { # configuration syntax key's and type. MERGED
        'class?': type, # specific class of landscape
        'instance?': '*', # landscape instance
        'random_seed': [int, None], # for random number instance
        'invert?': bool,
        'offset?': float,
        'parameters': [None, int],   # may be used by subclasses
        'size': {   # size should be used for all genome size references
            'min': int,
            'max': int,
            'exact': int
        },
    }
    default = { # default syntax values. MERGED
        'random_seed': None,
        'invert': False,
        'offset': 0.0,
        'parameters': None,
        'size': {
            'min': 0,
            'max': 0,
            'exact': 0
        },
    }
    
    test_key = () # test keys used for linear configuration strings
    test_cfg = () # tuple of linear configuration strings
    
    strict = {} # eg. 'size.exact':2  <- NOT merged like syntax/default
    
    def __init__(self, cfg=None, **other_cfg):
        '''Initialises the landscape.
        
        This method should be overridden to perform any once-off setup
        that is required, such as generating a landscape map or sequence
        of test cases.
        
        :Warn:
            This initialiser is called *before* the system is
            constructed. Importantly, the members of `esec.context` have
            not been initialised and will raise exceptions if accessed.
        '''
        self.syntax = merge_cls_dicts(self, 'syntax') # all in hierarchy
        self.cfg = ConfigDict(merge_cls_dicts(self, 'default'))
        
        if cfg is None: cfg = ConfigDict()
        elif not isinstance(cfg, ConfigDict): cfg = ConfigDict(cfg)
        
        self.cfg.overlay(cfg)
        for key, value in other_cfg.iteritems():
            if key in self.syntax:
                self.cfg.set_by_name(key, value)
            elif key.partition('_')[0] in self.syntax:
                self.cfg.set_by_name(key.replace('_', '.'), value)
        
        cfg_validate(self.cfg, self.syntax, self.ltype + ':' + self.lname)
        
        # Initialise size properties
        self.size = self.cfg.size
        if self.size_equals_parameters and self.cfg.parameters: self.size.exact = self.cfg.parameters
        if self.size.exact: self.size.min = self.size.max = self.size.exact
        if self.size.min >= self.size.max: self.size.exact = self.size.max = self.size.min
        
        # Now check for any strict limits (ie parameters)
        cfg_strict_test(self.cfg, self.strict)
        
        # random seed?
        if not isinstance(self.cfg.random_seed, int):
            random.seed()
            self.cfg.random_seed = cfg.random_seed = random.randint(0, sys.maxint)
        self.rand = Random(self.cfg.random_seed)
        
        # inversion? offset?
        self.invert = self.cfg.invert
        self.offset = self.cfg.offset
        
        # Autobind _eval_minimise or _eval_maximise.
        if not hasattr(self, 'eval'):
            if self.maximise == self.invert:
                setattr(self, 'eval', self._eval_minimise)
            else:
                setattr(self, 'eval', self._eval_maximise)
    
    def _eval_maximise(self, indiv):
        '''Evaluates the provided individual and wraps the result in a
        `FitnessMaximise` object.
        '''
        fitness = self._eval(indiv)     #pylint: disable=E1101
        if isinstance(fitness, Fitness): return fitness
        else: return FitnessMaximise(fitness + self.offset)
    
    def _eval_minimise(self, indiv):
        '''Evaluates the provided individual and wraps the result in a
        `FitnessMinimise` object.
        '''
        fitness = self._eval(indiv)     #pylint: disable=E1101
        if isinstance(fitness, Fitness): return fitness
        else: return FitnessMinimise(fitness + self.offset)
    
    def legal(self, indiv): #pylint: disable=W0613,R0201
        '''Determines whether the specified individual is legal.
        
        By default, this function always returns ``True``. Subclasses
        may override this to perform other verification appropriate to
        the landscape.
        
        :See: esec.individual.Individual.legal
        '''
        return True
    
    @classmethod
    def by_cfg_str(cls, cfg_str):
        '''Used by test framework to initialise a class instance using a
        simple test string specified in each class.test_cfg as nested
        tuples.
        
        :rtype: Landscape
        '''
        # create ConfigDict using cfg_str (defaults not needed but why not)
        cfg = ConfigDict()
        # map string to appropriate keys and types (or nested keys)
        cfg.set_linear(cls.test_key, cfg_str)
        # provide a new instance
        return cls(cfg)
    
    def info(self, level):
        '''Return landscape info for any landscape
        '''
        result = ['Using %s %s landscape' % (a_or_an(self.lname), self.lname)]
        if level > 3:
            result.append('')
            result.append('Configuration:')
            result.extend(self.cfg.lines())
        return result