Source code for genes

"""Handles node and connection genes."""
import warnings
from random import random

from neat.attributes import FloatAttribute, BoolAttribute, StringAttribute


# TODO: There is probably a lot of room for simplification of these classes using metaprogramming.
# TODO: Evaluate using __slots__ for performance/memory usage improvement.


[docs]class BaseGene(object): """ Handles functions shared by multiple types of genes (both node and connection), including crossover and calling mutation methods. """ def __init__(self, key): self.key = key
[docs] def __str__(self): attrib = ['key'] + [a.name for a in self._gene_attributes] attrib = [f'{a}={getattr(self, a)}' for a in attrib] return f'{self.__class__.__name__}({", ".join(attrib)})'
[docs] def __lt__(self, other): assert isinstance(self.key, type(other.key)), f"Cannot compare keys {self.key!r} and {other.key!r}" return self.key < other.key
[docs] @classmethod def parse_config(cls, config, param_dict): pass
[docs] @classmethod def get_config_params(cls): params = [] if not hasattr(cls, '_gene_attributes'): setattr(cls, '_gene_attributes', getattr(cls, '__gene_attributes__')) warnings.warn( f"Class '{cls.__name__!s}' {cls!r} needs '_gene_attributes' not '__gene_attributes__'", DeprecationWarning) for a in cls._gene_attributes: params += a.get_config_params() return params
@classmethod def validate_attributes(cls, config): for a in cls._gene_attributes: a.validate(config)
[docs] def init_attributes(self, config): for a in self._gene_attributes: setattr(self, a.name, a.init_value(config))
[docs] def mutate(self, config): for a in self._gene_attributes: v = getattr(self, a.name) setattr(self, a.name, a.mutate_value(v, config))
[docs] def copy(self): new_gene = self.__class__(self.key) for a in self._gene_attributes: setattr(new_gene, a.name, getattr(self, a.name)) return new_gene
[docs] def crossover(self, gene2): """ Creates a new gene randomly inheriting attributes from its parents.""" assert self.key == gene2.key # Note: we use "a if random() > 0.5 else b" instead of choice((a, b)) # here because `choice` is substantially slower. new_gene = self.__class__(self.key) for a in self._gene_attributes: if random() > 0.5: setattr(new_gene, a.name, getattr(self, a.name)) else: setattr(new_gene, a.name, getattr(gene2, a.name)) return new_gene
# TODO: Should these be in the nn module? iznn and ctrnn can have additional attributes.
[docs]class DefaultNodeGene(BaseGene): _gene_attributes = [FloatAttribute('bias'), FloatAttribute('response'), StringAttribute('activation', options=''), StringAttribute('aggregation', options='')] def __init__(self, key): assert isinstance(key, int), f"DefaultNodeGene key must be an int, not {key!r}" BaseGene.__init__(self, key)
[docs] def distance(self, other, config): d = abs(self.bias - other.bias) + abs(self.response - other.response) if self.activation != other.activation: d += 1.0 if self.aggregation != other.aggregation: d += 1.0 return d * config.compatibility_weight_coefficient
# TODO: Do an ablation study to determine whether the enabled setting is # important--presumably mutations that set the weight to near zero could # provide a similar effect depending on the weight range, mutation rate, # and aggregation function. (Most obviously, a near-zero weight for the # `product` aggregation function is rather more important than one giving # an output of 1 from the connection, for instance!)
[docs]class DefaultConnectionGene(BaseGene): _gene_attributes = [FloatAttribute('weight'), BoolAttribute('enabled')] def __init__(self, key): assert isinstance(key, tuple), f"DefaultConnectionGene key must be a tuple, not {key!r}" BaseGene.__init__(self, key)
[docs] def distance(self, other, config): d = abs(self.weight - other.weight) if self.enabled != other.enabled: d += 1.0 return d * config.compatibility_weight_coefficient