PK= self.steps
class FitnessBound(StopCondition):
""" If the genetic algorithm obtained a fine enough individual. """
def __init__(self, fitness_bound, all_populations=False):
""" Initializes this function with the upper bound for the fitness.
:param fitness_bound: An fitness value.
:param all_populations: If True, the condition will be met only when all
the populations contain at least one individual with a fitness
higher than the bound. If False, only one individual among all the
populations will suffice.
"""
self.fitness_bound = fitness_bound
self.all_populations = all_populations
def __call__(self, genetic_algorithm):
""" Checks if this stop criteria is met.
It will look at the generation of the genetic algorithm. It's expected
that. If its generation is greater or equal to the specified in
initialization method, the criteria is met.
:param genetic_algorithm: The genetic algorithm where this stop
condition belongs.
:return: True if criteria is met, false otherwise.
"""
fitnesses = [p[0].fitness() for p in genetic_algorithm.populations]
criteria = [fitness > self.fitness_bound for fitness in fitnesses]
return all(criteria) if self.all_populations else any(criteria)
PK:Hp pynetics/recombination.pyimport abc
from .individuals import Individual
class Recombination(metaclass=abc.ABCMeta):
""" Defines the behaviour of a recombination operator.
A recombination operator takes a set of individuals (i.e. parents) and
generates a different set of individuals (i.e. offspring) normally with
aspects derived from their parents.
"""
def __call__(self, *args):
""" Applies the recombine method to a sequence of individuals.
:param args: A list of one or more Individual instances to use as
parents in the recombination.
:returns: A sequence of individuals with characteristics of the parents.
"""
if not args:
msg = 'At least one individual is required for recombination'
raise ValueError(msg)
elif not all([isinstance(i, Individual) for i in args]):
msg = 'All parameters should be Individual instances'
raise ValueError(msg)
else:
return self.perform(*args)
@abc.abstractmethod
def perform(self, *args):
""" Implementation of the recombine method.
The method will always receive a list of Individual instances, and the
implementation must be aware of the individual types because given that
not all implementations are the same, not all the crossover operations
may work.
:param args: A list of one or more Individual instances to use as
parents in the recombination.
:returns: A sequence of individuals with characteristics of the parents.
"""
class NoRecombination(Recombination):
""" A crossover method where no method is applied to the individuals. """
def perform(self, *args):
""" Return the same individuals passed as parameter. """
return args
PK:HЖpynetics/catastrophe.pyimport abc
# TODO I'm not proud of this methods. I think they performance may be improved.
from .utils import take_chances
class Catastrophe(metaclass=abc.ABCMeta):
""" Defines the behaviour of a genetic algorithm catastrophe operator.
It's expected for this operator to keep track of the ga and know when to act
since it will be called every step of the algorithm after replacement
operation.
"""
def __call__(self, population):
""" Tries to apply the catastrophic operator to the population.
This method does some checks and the delegates the application of the
catastrophic operator to the "perform" method.
:param population: The population where apply the catastrophic method.
"""
if population is None:
raise ValueError('The population cannot be None')
else:
return self.perform(population)
@abc.abstractmethod
def perform(self, population):
""" Implementation of the catastrophe operation.
:param population: the population which may suffer the catastrophe
"""
class NoCatastrophe(Catastrophe):
""" A catastrophe method where nothing happens. """
def perform(self, population):
""" It's a wonderful world and nothing happens.
:param population: The poppulation where nothing happens. Ever.
"""
pass
class ProbabilityBasedCatastrophe(Catastrophe, metaclass=abc.ABCMeta):
""" Base class for some bundled probability based catastrophe methods.
This method will have a probability to be triggered. Is expected this
probability to be very little.
"""
def __init__(self, probability):
""" Initializes this catastrophe method.
:param probability: The probability fot the catastrophe to happen.
"""
self.__probability = probability
def perform(self, population):
if take_chances(self.__probability):
self.perform_catastrophe()
@abc.abstractmethod
def perform_catastrophe(self, population):
""" Returns a list of the individuals to remove from population.
:param population: The population from where extract individuals.
:return: The individuals to retain after the catastrophe application.
"""
class PackingByProbability(ProbabilityBasedCatastrophe):
""" Replaces all repeated individuals maintaining only one copy of each. """
def perform_catastrophe(self, population):
""" Replaces all repeated individuals by new ones.
:param population: The population where apply the catastrophe.
"""
visited_individuals = []
for i in range(len(population)):
if population[i] in visited_individuals:
population[i] = population.spawn()
visited_individuals.append(population[i])
class DoomsdayByProbability(ProbabilityBasedCatastrophe):
""" Replaces all but the best individual. """
def perform_catastrophe(self, population):
""" Replaces all the individuals but the best.
:param population: The population where apply the catastrophe.
"""
for i in range(1, len(population)):
population[i] = population.spawning_pool.create()
PK:H@ppynetics/exceptions.pyclass GeneticAlgorithmError(Exception):
# TODO TBD
pass
class UnexpectedClassError(GeneticAlgorithmError):
""" Raised when an instance is not of the expected class. """
def __init__(self, expected_class):
""" Initializes the exception.
:param expected_class: The expected class for that instance.
"""
super().__init__('Expected class {}'.format(expected_class))
class WrongValueForInterval(ValueError):
""" When a value does not belong to an interval. """
def __init__(
self,
var_name,
lower,
upper,
value,
inc_lower=True,
inc_upper=True,
):
""" Initializes the exception.
:param var_name: The variable name which contains the wrong value.
:param lower: The lower bound of the interval.
:param upper: The upper bound of the interval.
:param value: The value.
:param inc_lower: If the lower bound is include. Defaults to True.
:param inc_upper: If the upper bound is include. Defaults to True.
"""
self.lower = lower
self.upper = upper
self.var_name = var_name
self.value = value
self.inc_lower = inc_lower
self.inc_upper = inc_upper
msg = 'Expected {} ∈ {}{}, {}{} but got {}'.format(
var_name,
'[' if inc_lower else '(',
self.lower,
self.upper,
']' if inc_upper else ')',
self.value,
)
super().__init__(msg)
class NotAProbabilityError(WrongValueForInterval):
""" If a value is not a valid probability. """
def __init__(self, var_name, value):
""" Initializes the instance.
:param var_name: The variable name which contains the wrong value.
:param value: The value.
"""
super().__init__(var_name, 0, 1, value, inc_lower=True, inc_upper=True)
PK:H wwpynetics/mutation.pyimport abc
from pynetics.utils import check_is_instance_of
from pynetics.individuals import Individual
class Mutation(metaclass=abc.ABCMeta):
""" Defines the behaviour of a genetic algorithm mutation operator. """
def __call__(self, individual):
""" Applies the crossover method to the list of individuals.
:param individual: an individual to mutate.
:returns: A new mutated individual.
:raises UnexpectedClassError: If the individual is not an Individual
instance.
"""
individual = check_is_instance_of(individual, Individual)
return self.perform(individual)
@abc.abstractmethod
def perform(self, individual):
""" Implementation of the mutation operation.
The mutation implementation must be aware of the implementation type.
Given that not all the implementations are the same, not all the
mutation operations may work.
:param individual: The individual to mutate.
:returns: A new mutated individual.
"""
class NoMutation(Mutation):
""" A method where no modification is performed to the individual. """
def perform(self, individual):
""" Return the same individual passed as parameter.
:param individual: The individual to mutate.
:returns: The same, unmodified individual.
"""
return individual
PK:H
!!pynetics/individuals.pyimport abc
class Individual:
""" One of the possible solutions to a problem.
In a genetic algorithm, an individual is a tentative solution of a problem,
i.e. the environment where populations of individuals evolve.
"""
def __init__(self):
""" Initializes the individual. """
self.population = None
def fitness(self):
""" Cumputes the fitness of this individual.
It will use the fitness method defined on its population.
:return: A fitness.
"""
return self.population.fitness(self)
@abc.abstractmethod
def phenotype(self):
""" The expression of this particular individual in the environment.
:return: An object representing this individual in the environment
"""
class SpawningPool(metaclass=abc.ABCMeta):
""" Defines the methods for creating individuals required by population. """
@abc.abstractmethod
def create(self):
""" Creates a new individual randomly.
:return: A new Individual object.
"""
PK:HBpynetics/utils.pyimport random
from .exceptions import UnexpectedClassError
def take_chances(probability=0.5):
""" Given a probability, the method generates a random value to see if is
lower or not than that probability.
:param probability: The value of the probability to beat. Default is 0.5.
:return: A value of True if the value geneated is bellow the probability
specified, and false otherwise.
"""
return random.random() < probability
# Validations
def check_is_instance_of(value, cls):
""" Checks if a value is instance of a given class.
If the value is an instance of the class, he method will return the value as
is. Otherwise, it will raise an error.
:param value: The value to be checked.
:param cls: The class to be checked on.
:return: The value.
:raises UnexpectedClassError: In case of the value is not an instance of the
given class.
"""
if not isinstance(value, cls):
raise UnexpectedClassError(cls)
else:
return value
PK:HQmwpynetics/gggp/__init__.py__author__ = 'blazaid'
PK:HiPPpynetics/gggp/grammar.pyimport abc
import collections
import operator
import random
from pynetics import take_chances
class Term(metaclass=abc.ABCMeta):
""" Any of the elements in the right part of a production.
A term is an element that represents terminals, non-terminals, connectors,
multipliers or any combination of them.
"""
def __init__(self, value):
""" Initializes the term with a value.
:param value: The value for this term. Is expected that subclasses
implements the required validations for this value as Term will only
maintain its value.
"""
self.__value = value
@property
def value(self):
""" Returns the value of this term. """
return self.__value
@abc.abstractmethod
def random_derivation(self):
""" Generates a random derivation of this term.
The method should give the immediate terms managed under this term. In
case of terms like SingleTerm or And, the underneath terms are imposed,
but in case of terms like Or or OneOrMany, the method will return a
random list of elements (based on the weights, probabilities or
whatever the term is using).
Don't make any recursive call in implementations. Return the immediate
terms; the method "random_tree" of Grammar class will take care of
generating the tree.
:return: A tuple of the immediate terms (or strings) managed under this
term.
"""
class Connector(Term, metaclass=abc.ABCMeta):
""" A Connector instance holds many elements.
This elements should be instances of Term class and the order will be
maintained.
"""
def __init__(self, *args):
""" Initializes this instance with the values.
:param args: All the terms to be considered in this connector. It
should contain at least 2 terms and all the elements should be
instances of Term class.
:raises ValueError: If the value is not a tuple with strings, instances
of Term or both.
"""
super().__init__(value=self.__process_args(args))
@staticmethod
def __process_args(args):
""" Checks that value is an instance of string or Term. """
error_msg = None
if not isinstance(args, tuple):
error_msg = 'should be an instance of tuple'
elif len(args) <= 1:
error_msg = 'should have at least 2 elements'
elif not all([isinstance(term, (Term, str)) for term in args]):
error_msg = 'elements should be Term or string instances'
if error_msg:
raise ValueError('Parameter "args" ' + error_msg)
else:
return tuple(
SingleTerm(t) if isinstance(t, str) else t
for t in args
)
def __eq__(self, other):
""" Tell if this instance is equal to other by its content. """
if self is other:
return True
elif not isinstance(other, Term) or len(self.value) != len(other.value):
return False
else:
return all([t1 == t2 for t1, t2 in zip(self.value, other.value)])
class And(Connector):
""" This connector represents an And connector.
This connector holds a tuple of terms that should appear in the stated
order.
"""
def random_derivation(self):
""" This term returns all the managed terms. """
return self.value
class Or(Connector):
""" This connector represents an Or connector.
It holds a tuple of terms from which to choose one. At initialization time
a list of tuples may be specified instead a list of terms. In that case,
each tuple should be expressed in the form (t, w) where t is the tuple and w
the relative weight of that tuple over other. For example, if the input
terms is as follows:
term = Or((t_1, 2), (t_2, 1))
Then t_1 has double chances to be selected over t_2.
"""
def __init__(self, *args):
""" Initializes the instance.
:param value: A list of terms or a list of tuples in the form (t, w)
where t is the term and w the relative weight of the term over the
others in the list.
"""
value, weights = self.__process_value(args)
super().__init__(*value)
self.__weights = self.__validate_weights(weights)
@staticmethod
def __process_value(value):
""" Checks value is a tuple of tuples and processes it.
If it's a tuple but not a tuple of tuples, it will transform it to a
tuple of tuples where the second term of each nested tuple is a 1.
:return: A tuple of two tuples, one of the terms and the other with
their associated weights.
"""
if not isinstance(value, tuple):
raise ValueError("Parameter value should be a tuple")
else:
possible_tuples = [isinstance(t, tuple) for t in value]
if any(possible_tuples) and not all(possible_tuples):
msg = 'Parameter value should be terms or tuples but not both'
raise ValueError(msg)
elif not all(possible_tuples):
return value, tuple(1 for _ in value)
else:
return (
tuple(t for t, _ in value),
tuple(w for _, w in value),
)
@staticmethod
def __validate_weights(weights):
""" Checks the weights are all int or float instances. """
if not all([isinstance(w, (int, float)) for w in weights]):
raise ValueError("Parameter value contains invalid weights")
else:
return weights
@property
def weights(self):
""" Returns the weights for the values managed under this term. """
return self.__weights
def __eq__(self, o):
""" Tell if this instance is equal to other by its content. """
if super().__eq__(o):
return all([w1 == w2 for w1, w2 in zip(self.weights, o.weights)])
else:
return False
def random_derivation(self):
""" This term returns a tuple with one of its managed terms.
The odds for a term to be chosen will be affected by its weight.
"""
total_w = float(sum(self.weights))
terms_with_prob = sorted(
[
(t, w / total_w) for t, w in
zip(self.value, self.weights)
], key=operator.itemgetter(1)
)
selected_p = random.random()
p = 0
for element in terms_with_prob:
p = p + element[1]
if selected_p <= p:
return element[0],
return terms_with_prob[-1],
class Multiplier(Term, metaclass=abc.ABCMeta):
""" A Multiplier instance that holds a term that may appear many times. """
def __init__(self, value, *, lower=None, upper=None, p=0.5):
""" Initializes this instance with the value.
:param value: The term to be managed. It should be a Term instance other
than a Multiplier instance.
:param lower: The minimum times the term should appear according to the
grammar. It defaults to 0.
:param upper: The maximum times the term may appear. It defaults to
infinite, meaning that there is no limit of the times the term may
appear.
:param p: In case of generating terms, this param says w
:raises ValueError: If the value is not a Term instance.
"""
super().__init__(value=self.__process_value(value))
self.__lower, self.__upper = self.__process_limits(lower, upper)
self.__p = self.__check_probability(p)
@staticmethod
def __process_value(value):
""" Processes that the value, validating it.
For this purpose it will check if value is an instance of Term (other
than a NToM term) or a string. If it's the former, it will return it. If
it's the latter, it will return a SingleTerm with the string as it's
content. If it's not a Term or a string, or if it's a Multiplier, the
method will raise a ValueError.
"""
error_msg = None
if not isinstance(value, (Term, str)):
error_msg = 'should be an Term instance (but not a Multiplier one)'
elif isinstance(value, Multiplier):
error_msg = 'cannot be a NToM instance'
if error_msg:
raise ValueError('Parameter "value" ' + error_msg)
else:
return SingleTerm(value) if isinstance(value, str) else value
@staticmethod
def __process_limits(lower, upper):
""" Ensure that limits are specified correctly.
:param lower: The lower bound of the interval.
:param upper: The upper bound of the interval.
:return: The correct lower and upper bounds.
:raises ValueError: If any of the bounds isn't correct (e.g. not an
integer) or if the upper bound is not greater than the lower bound.
"""
try:
lower = int(lower) if lower is not None else 0
upper = int(upper) if upper is not None else float('Inf')
if lower < upper:
return lower, upper
else:
ValueError('Upper limit should be greater than lower limit')
except ValueError:
raise ValueError('Lower or upper is not an integer')
@staticmethod
def __check_probability(p):
""" Checks p is valid, i.e. a float value x ∈ [0.0, 1.0]. """
if p is None or not isinstance(p, float) or not (0. <= p <= 1.):
error_msg = 'The term p should be an float x ∈ [0.0, 1.0]'
raise ValueError(error_msg)
else:
return p
def __eq__(self, other):
""" Tell if this instance is equal to other by its content. """
if self is other:
return True
else:
eq_limits = self.lower == other.lower and self.upper == other.upper
eq_p = self.p == other.p
eq_value = self.value == other.value
return isinstance(other, Term) and eq_limits and eq_p and eq_value
@property
def lower(self):
""" Returns the minimum times a the managed term should appear. """
return self.__lower
@property
def upper(self):
""" Returns the maximum times this term may appear. """
return self.__upper
@property
def p(self):
""" Returns the probability of a new appearance of this term. """
return self.__p
def random_derivation(self):
""" Returns a tuple with the managed term according to limits and p.
The minimum size of the returned tuple will be the lower limit
specified in the init param "lower". After that, and with a maximum
of "upper" terms, a new param will be added as long as random tests fall
under the probability specified.
"""
terms = [self.value for _ in range(self.lower)]
while len(terms) < self.upper and take_chances(self.p):
terms.append(self.value)
return tuple(terms)
class SingleTerm(Term):
""" A single term can hold one and only one element.
This element must be a string (i.e. a variable name or a terminal).
"""
def __init__(self, value):
""" Initializes this term with the given value .
:param value: The value of the term. It should be an string instance.
:raises ValueError: If the value is not an string.
"""
super().__init__(value=self.__check_value(value))
@staticmethod
def __check_value(value):
""" Checks that value is an instance of string. """
if value is None or not isinstance(value, str):
error_msg = 'The term value should be an instance of string'
raise ValueError(error_msg)
else:
return value
def __eq__(self, other):
""" Tell if this instance is equal to other by it's content. """
if self is other:
return True
elif not isinstance(other, Term):
return False
else:
return self.value == other.value
def random_derivation(self):
""" Returns a tuple with a number of terms between zero and n. """
return self.value,
EpsilonTerminal = SingleTerm('ε')
class Production:
""" A production of the grammar.
A production rule is a pair of elements (v, t) where v is a grammar variable
(an element which "produces" or "is replaced by" something) and t is a term
of terminal and non-terminal elements (that something. See Term for more
information).
"""
def __init__(self, variable, term):
""" Initializes the production rule with its information.
:param variable: The variable that produces or is replaced by terms.
:param term: The term of the production. Should be any of the instances
of Term, otherwise it will raise an error.
:raises ValueError: If term is not a Term instance.
"""
self.__variable = variable
self.__term = self.__validate_term(term)
@staticmethod
def __validate_term(term):
""" Checks and returns the term if there is no error.
If term is invalid, a ValueError with the error message will be raised.
"""
if not isinstance(term, Term):
raise ValueError('Parameter "term" should be a Term instance')
else:
return term
@property
def variable(self):
""" Returns the alpha part of this production rule. """
return self.__variable
@property
def term(self):
""" Returns the beta part of this production rule. """
return self.__term
def __eq__(self, other):
if not self.variable == other.variable:
return False
else:
return self.term == other.term
class Node:
def __init__(self, value, parent, processed=False):
self.__value = value
self.__parent = parent
self.__processed = processed
@property
def value(self):
return self.__value
@property
def parent(self):
return self.__parent
@property
def processed(self):
return self.__processed
@processed.setter
def processed(self, processed):
self.__processed = processed
def __str__(self):
return self.value
class Grammar:
""" An object which represents a context free grammar. """
def __init__(self, start_symbol=None, productions=None):
""" Initializes the grammar.
:param start_symbol: The start symbol of this grammar. Should exist in
the productions as a variable. Otherwise it will raise an error.
:param productions: The list of productions which defines this grammar.
It should be defined as a list of production instances, otherwise it
will raise an error. If there are different productions with the
same variable, they will be shrunk to one single production with an
Or term connector with their terms.
:raises ValueError: If "start_symbol" parameter is not present or if it
exists, it's not empty and does not exists in any of the production
rules.
:raises ValueError: If "production_rules" parameter is not present or if
it is present but is not a valid list of production rules.
"""
self.__productions = self.__process_productions(productions)
self.__start_symbol = self.__process_start_symbol(
start_symbol,
productions,
)
self.__variables = None
self.__terminals = None
@staticmethod
def __process_productions(productions):
""" Validates the productions raising an error if it's wrong. """
error_msg = None
if not productions or not isinstance(productions, list):
error_msg = 'should be a valid list'
elif not all([isinstance(p, Production) for p in productions]):
error_msg = 'contains at least one non-Production instance'
if error_msg:
raise ValueError('Parameter "production_rules" ' + error_msg)
# Everything is ok at this point, so let's shrink the productions
all_prods = collections.defaultdict(lambda: [])
for production in productions:
all_prods[production.variable].append(production.term)
new_productions = []
for variable, terms in all_prods.items():
new_productions.append(Production(
variable=variable,
term=Or(*terms) if len(terms) > 1 else terms[0],
))
return new_productions
@staticmethod
def __process_start_symbol(start_symbol, productions):
""" Validates the start symbol raising an error if it's wrong. """
if start_symbol not in [p.variable for p in productions]:
raise ValueError('Parameter "start_symbol" not in productions')
else:
return start_symbol
@property
def start_symbol(self):
""" Returns the start symbol of this grammar. """
return self.__start_symbol
@property
def productions(self):
""" Returns the production rules for this grammar. """
return self.__productions
@property
def variables(self):
""" Returns the variables for this grammar. """
if self.__variables is None:
self.__variables = set(p.variable for p in self.productions)
return self.__variables
@property
def terminals(self):
""" Returns the terminals for this grammar. """
if self.__terminals is None:
productions = {p.variable: p.term for p in self.productions}
elements = [self.start_symbol]
self.__terminals = set()
while elements:
element = elements.pop()
if isinstance(element, (SingleTerm, Multiplier)):
elements.append(element.value)
elif isinstance(element, Connector):
elements.extend(element.value)
elif element in self.variables:
elements.append(productions[element])
else:
self.__terminals.add(element)
return self.__terminals
def random_tree(self, initial_symbol=None):
""" Creates a random tree using the specified initial symbol. """
# I've made this iterative alg. because the recursive one failed a lot
productions = {p.variable: p.term for p in self.productions}
root = initial_symbol or self.__start_symbol
tree = DerivationTree(self, [Node(value=root, parent=None, )])
i = 0
while i < len(tree):
node = tree[i]
if node.processed:
i += 1
else:
node.processed = True
if isinstance(node.value, Term):
elements = [
Node(value=element, parent=node.parent)
for element in node.value.random_derivation()
]
elif node.value in self.variables:
elements = [
node,
Node(
value=productions[node.value],
parent=i,
)
]
else:
# node.value in self.terminals
elements = [node]
tree[i:i + 1] = elements
i = 0
return tree
class DerivationTree(list):
def __init__(self, grammar, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__grammar = grammar
self.__word = None
def word(self):
if self.__word is None:
word = [self[0]]
i = 0
while i < len(word):
node = word[i]
if node.value in self.grammar.terminals:
i += 1
else:
children = [
n for n in self
if n.parent == self.index(node)
]
word[i:i + 1] = children
i = 0
self.__word = tuple(n.value for n in word)
return self.__word
@property
def grammar(self):
return self.__grammar
PK:H]
LLpynetics/gggp/main.pyfrom pprint import pprint
from pynetics.gggp import grammar
awesome_grammar = grammar.Grammar(
start_symbol='frase',
productions=[
grammar.Production(
'frase',
grammar.And(
grammar.Multiplier('sujeto', upper=1),
'predicado',
grammar.Multiplier(
grammar.And(
'conjuncion',
grammar.Multiplier('sujeto', upper=1),
'predicado'
),
)
),
),
grammar.Production('conjuncion', grammar.Or(('y', 4), ('o', 6))),
grammar.Production('sujeto', grammar.Or('sujeto_masc', 'sujeto_fem')),
grammar.Production(
'sujeto_masc',
grammar.Or(
grammar.And('el', 'nombre_comun_masc'),
grammar.And(
grammar.Multiplier('el', upper=1),
'nombre_propio_masc'
),
)
),
grammar.Production(
'sujeto_fem',
grammar.Or(
grammar.And('la', 'nombre_comun_fem'),
grammar.And(
grammar.Multiplier('la', upper=1),
'nombre_propio_fem'
),
)
),
grammar.Production(
'nombre_comun_masc',
grammar.Or('chico', 'chatarrero', 'profesor', 'mutante', 'zombie'),
),
grammar.Production(
'nombre_propio_masc',
grammar.Or('Pepe', 'Paco Pil', 'Richal'),
),
grammar.Production(
'nombre_comun_fem',
grammar.Or('camionera', 'guitarrista', 'prestituta', 'tabernera'),
),
grammar.Production(
'nombre_propio_fem',
grammar.Or('Juani', 'Pepi', 'Lili'),
),
grammar.Production(
'predicado',
grammar.And('verbo', grammar.Multiplier('complemento', upper=1))
),
grammar.Production(
'verbo',
grammar.Or(
'corre',
'habla',
'ríe',
'tiene',
'va',
'come',
'dice',
'canta',
'caga',
'mea',
'micciona',
'excreta',
'evacúa',
)
),
grammar.Production(
'complemento',
grammar.Or(
'la comida',
'como puede',
'que se las pela',
'soy una rumbera',
'abriendo puertas',
'a las barricadas',
'algo',
'siempre',
'a dos manos',
)
),
]
)
pprint(' '.join(awesome_grammar.random_tree().word()))
PK:H s}}pynetics/ga_list/ga_bin.pyimport random
from pynetics import ga_list
class BinaryIndividualSpawningPool(ga_list.ListIndividualSpawningPool):
""" Defines the methods for creating binary individuals. """
def __init__(self, size):
""" Initializes this spawning pool for generating list individuals.
:param size: The size of the individuals to be created from this
spawning pool.
"""
super().__init__(size, binary_alleles)
class GeneralizedRecombination(ga_list.ListRecombination):
""" Offspring is obtained by crossing individuals as they where integers.
NOTE: Works only for individuals with list chromosomes of binary alleles.
Ok, may work for other individuals with list chromosomes, but the results
may be strange (perhaps better, but I doubt it)
"""
parents_num = 2
def perform(self, individuals):
""" Applies the crossover operator.
:param individuals: The individuals to cross to generate progeny.
:return: A list of individuals.
"""
i1, i2 = individuals[0], individuals[1]
# Obtain the crossover range (as integer values)
a = int(''.join([str(b1 & b2) for (b1, b2) in zip(i1, i2)]), 2)
b = int(''.join([str(b1 | b2) for (b1, b2) in zip(i1, i2)]), 2)
# Get the children (as integer values)
c = random.randint(a, b)
d = b - (c - a)
# Convert to binary lists again (NOTE: we remove the starting 0b)
bin_c = [int(x) for x in bin(c)[2:]]
bin_d = [int(x) for x in bin(d)[2:]]
# Convert to chromosomes and we're finish
child1, child2 = i1.population.spawn(), i2.population.spawn()
for i in range(len(bin_c)):
child1[i], child2[i] = bin_c[i], bin_d[i]
return [child1, child2, ]
# A Finite set of alleles where the only valid values are 0 and 1.
binary_alleles = ga_list.FiniteSetAlleles((0, 1))
PK:HWk))pynetics/ga_list/__init__.pyimport abc
import random
import collections
from pynetics.individuals import SpawningPool, Individual
from pynetics.recombination import Recombination
from pynetics.mutation import Mutation
from pynetics.utils import take_chances, check_is_instance_of
class Alleles(metaclass=abc.ABCMeta):
""" The alleles are all the possible values a gene can take. """
@abc.abstractmethod
def get(self):
""" Returns a random value of all the possible existent values. """
class FiniteSetAlleles(Alleles):
""" The possible alleles belong to a finite set of symbols. """
def __init__(self, values):
""" Initializes this set of alleles with its sequence of symbols.
The duplicated values are removed in the list maintained by this alleles
class, but the order is maintained.
:param values: The sequence of symbols.
"""
# TODO Tiene toda la pinta de que se pueda mantener como Sequence
values = check_is_instance_of(values, collections.Sequence)
self.__values = list(collections.OrderedDict.fromkeys(values))
def get(self):
""" A random value is selected uniformly over the set of values. """
return random.choice(self.__values)
@property
def values(self):
""" Returns the list of values for genes allowed by this instance. """
return self.__values[:]
class ListIndividualSpawningPool(SpawningPool, metaclass=abc.ABCMeta):
""" Defines the methods for creating individuals required by population. """
def __init__(self, size, alleles):
""" Initializes this spawning pool for generating list individuals.
:param size: The size of the individuals to be created from this
spawning pool.
:param alleles: The alleles to be used as values of the genes.
"""
self.__size = size
self.alleles = alleles
def create(self):
""" Creates a new individual randomly.
:return: A new Individual object.
"""
individual = ListIndividual()
for _ in range(self.__size):
individual.append(self.alleles.get())
return individual
class ListIndividual(Individual, list):
""" An individual whose representation is a list of finite values. """
def __eq__(self, individual):
""" The equality between two list individuals is True if they:
1. Have the same length
2. Any two genes in the same position have the same value.
"""
return len(self) == len(individual) and all(
[x == y for (x, y) in zip(self, individual)]
)
def phenotype(self):
return self[:]
class ListRecombination(Recombination, metaclass=abc.ABCMeta):
""" Common behavior for crossover methods over ListIndividual instances. """
def __call__(self, *args):
""" Performs some checks before applying the crossover method.
Specifically, it checks if the length of all individuals are the same.
In so, the crossover operation is done. If not, a ValueError is raised.
:param args: The individuals to cross to generate progeny.
:return: A list of individuals with characteristics of the parents.
:raises ValueError: If not all the individuals has the same length.
"""
lengths = [len(i) for i in args]
if not lengths.count(lengths[0]) == len(lengths):
raise ValueError('Both individuals must have the same length')
else:
return super().__call__(*args)
class OnePointRecombination(ListRecombination):
""" Offspring is created by mixing the parents using one random pivot point.
This crossover implementation works with two (and only two) individuals of
type ListIndividual (or subclasses).
"""
def __call__(self, parent1, parent2):
""" Performs some checks before applying the crossover method.
Specifically, the constructor forces the number of parameters to 2.
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of individuals with characteristics of the parents.
:raises ValueError: If not all the individuals has the same length.
"""
return super().__call__(parent1, parent2)
def perform(self, parent1, parent2):
""" Offspring is obtained mixing the parents with one pivot point.
One example:
parents : aaaaaaaa, bbbbbbbb
pivot : 3
-----------
children : aaabbbbb, bbbaaaaa
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of two individuals, each a child containing some
characteristics from their parents.
"""
child1, child2 = parent1.population.spawn(), parent1.population.spawn()
p = random.randint(1, len(parent1) - 1)
for i in range(len(parent1)):
if i < p:
child1[i], child2[i] = parent1[i], parent2[i]
else:
child1[i], child2[i] = parent2[i], parent1[i]
return [child1, child2, ]
class TwoPointRecombination(ListRecombination):
""" Offspring is created by mixing the parents using two random pivot point.
This crossover implementation works with two (and only two) individuals of
type ListIndividual (or subclasses).
"""
def __call__(self, parent1, parent2):
""" Performs some checks before applying the crossover method.
Specifically, the constructor forces the number of parameters to 2.
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of individuals with characteristics of the parents.
:raises ValueError: If not all the individuals has the same length.
"""
return super().__call__(parent1, parent2)
def perform(self, parent1, parent2):
""" Offspring is obtained mixing the parents with two pivot point.
One example:
parents : aaaaaaaa, bbbbbbbb
pivot : 3, 5
-----------
children : aaabbaaa, bbbaabbb
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of two individuals, each a child containing some
characteristics from their parents.
"""
child1, child2 = parent1.population.spawn(), parent2.population.spawn()
pivots = random.sample(range(len(parent1) - 1), 2)
p, q = min(pivots[0], pivots[1]), max(pivots[0], pivots[1])
for i in range(len(parent1)):
if p < i < q:
child1[i], child2[i] = parent1[i], parent2[i]
else:
child1[i], child2[i] = parent2[i], parent1[i]
return [child1, child2, ]
class RandomMaskRecombination(ListRecombination):
""" Offspring is created by using a random mask.
This crossover implementation works with two (and only two) individuals of
type ListIndividual (or subclasses).
"""
def perform(self, individuals):
""" Offspring is obtained generating a random mask.
This mask determines which genes of each of the progenitors are used on
each of the the genes. For example:
parents : aaaaaaaa, bbbbbbbb
random mask : 00100110
-----------
children : aabaabba, bbabbaab
:param individuals: The individuals to cross to generate progeny.
:return: A list of two individuals, each a child containing some
characteristics from their parents.
"""
i1, i2 = individuals[0], individuals[1]
child1, child2 = i1.population.spawn(), i1.population.spawn()
for i in range(len(i1)):
if take_chances(.5):
child1[i], child2[i] = i1[i], i2[i]
else:
child1[i], child2[i] = i2[i], i1[i]
return [child1, child2, ]
class ListMutation(Mutation, metaclass=abc.ABCMeta):
""" Common behavior for mutation methods over ListIndividual instances. """
def __call__(self, individual):
""" Performs some checks before applying the mutate method.
Specifically, it checks if the individual is a ListIndividual or any of
its subclasses. If not, a ValueError is raised.
:param individual: The individual to be mutated.
:raises UnexpectedClassError: If the individual is not a subclass of the
class ListIndividual.
"""
return super().__call__(check_is_instance_of(
individual,
ListIndividual
))
class SwapGenes(ListMutation):
""" Mutates the by swapping two random genes.
This mutation method operates only with ListIndividuals (or any of their
subclasses.
"""
def perform(self, individual):
""" Swaps the values of two positions of the list of values.
When the individual is mutated, two random positions (pivots) are
generated. Then, the values of those positions are swapped. For example:
individual : 12345678
pivot : 3, 5
-----------
mutated : 12365478
:param individual: The individual to be mutated.
"""
genes = random.sample(range(len(individual) - 1), 2)
g1, g2 = genes[0], genes[1]
individual[g1], individual[g2] = individual[g2], individual[g1]
class RandomGeneValue(ListMutation):
""" Mutates the individual by changing the value to a random gene. """
def __init__(self, alleles):
""" Initializes the mutation method.
:param alleles: The alleles that the genes of the individual can take.
"""
self.__alleles = check_is_instance_of(alleles, Alleles)
@property
def alleles(self):
""" Returns the alleles that uses this mutation method. """
return self.__alleles
def perform(self, individual):
""" Changes the value of a random gene of the individual.
The mutated chromosome is obtained by changing a random gene as seen in
the next example:
individual : aabbaaba
alleles : (a, b, c, d)
change pos : 7
-----------
mutated : aabdaabc
:param individual: The individual to be mutated.
"""
i = random.choice(range(len(individual)))
new_gene = self.alleles.get()
while individual[i] == new_gene:
new_gene = self.alleles.get()
individual[i] = new_gene
return individual
PK9
pynetics/ga_list/ga_real.pyimport random
from pynetics.ga_list import Alleles, ListRecombination
class RealIntervalAlleles(Alleles):
""" The possible alleles are real numbers belonging to an interval. """
def __init__(self, a, b):
""" Initializes the alleles with the interval of all their valid values
It doesn't matters the order of the parameters. The interval will be all
the real numbers between the lower and the upper values.
:param a: One end of the interval.
:param b: Other end of the interval.
"""
self.__a = min(a, b)
self.__b = max(a, b)
def get(self) -> float:
""" A random value is selected uniformly over the interval. """
return random.uniform(self.__a, self.__b)
class MorphologicalRecombination(ListRecombination):
# TODO We need to improve this crossover.
"""Crossover that changes its behaviour depending on population diversity.
The idea is that, for each dimension of the vector (the chromosome of the
list individual) the algorithm first see how diverse is this dimension in
the population, that is, how big is the maximum difference between values
of different individuals in the same dimension. If the difference is to big,
the algorithm will choose the value of the children from a smaller interval
whereas if the diversity is to low, the algorithm will increase the interval
to increase the diversity.
NOTE: Works only for individuals with list chromosomes of real interval
alleles.
NOTE: The value of each gene must be normalized to the interval [0, 1].
"""
parents_num = 2
def __init__(self, a=-.001, b=-.133, c=.54, d=.226):
# TODO TBD Search the reference to the papers .
""" Initializes this crossover method.
The parameters a, b, c, d are the stated on paper: [INSERT HERE THE
REFERENCE]
:param a: One of the parameters.
:param b: Other parameter.
:param c: Yes, other parameter.
:param d: Ok, the last parameter is this.
"""
self.__a = a
self.__b = b
self.__c = c
self.__d = d
self.__calc_1 = (b - a) / c
self.__calc_2 = d / (1 - c)
self.__calc_3 = self.__calc_2 * -c
def perform(self, parent1, parent2):
""" Realizes the crossover operation.
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of two individuals, each one a child containing some
characteristics derived from the parents.
"""
child1, child2 = parent1.population.spawn(), parent2.population.spawn()
for g in range(len(parent1)):
genes_in_position_g = [i[g] for i in parent1.population]
diversity = max(genes_in_position_g) - min(genes_in_position_g)
phi = self.__phi(diversity)
lower_bound = min(parent1[g], parent2[g]) + phi
upper_bound = max(parent1[g], parent2[g]) - phi
child1[g] = random.uniform(lower_bound, upper_bound)
child2[g] = upper_bound - (child1[g] - lower_bound)
return [child1, child2, ]
def __phi(self, x):
""" Value in the interval from where to obtain the values grow or shrink
:param x: The value of the diversity
:return:
"""
if x <= self.__c:
return self.__calc_1 * x + self.__a
else:
return self.__calc_2 * x + self.__calc_3
PK:H6 pynetics/ga_list/ga_int.pyimport random
from pynetics import ga_list
class IntegerIndividualSpawningPool(ga_list.ListIndividualSpawningPool):
""" Defines the methods for creating integer individuals. """
def __init__(self, size, lower, upper):
""" Initializes this spawning pool for generating list individuals.
:param size: The size of the individuals to be created from this
spawning pool.
:param lower: the lower bound of the integer set (included).
:param upper: the upper bound of the integer set (included).
"""
super().__init__(
size,
ga_list.FiniteSetAlleles(range(lower, upper + 1))
)
self.lower = lower
self.upper = upper
class IntegerRangeRecombination(ga_list.ListRecombination):
""" Offspring is obtained by crossing individuals gene by gene.
For each gene, the interval of their values is calculated. Then, the
difference of the interval is used for calculating the new interval from
where to pick the values of the new genes. First, a value is taken from the
new interval. Second, the other value is calculated by taking the
symmetrical by the center of the range.
"""
def perform(self, parent1, parent2):
""" Applies the crossover operator.
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A list of individuals.
"""
i1_lower = parent1.population.spawning_pool.lower
i1_upper = parent1.population.spawning_pool.upper
i2_lower = parent2.population.spawning_pool.lower
i2_upper = parent2.population.spawning_pool.upper
child1, child2 = parent1.population.spawn(), parent2.population.spawn()
for i, genes in enumerate(zip(parent1, parent2)):
# For each gene, we calculate the the crossover interval. If the
# genes are equal, we take the whole possible interval
a, b = genes
if a != b:
diff = abs(a - b)
else:
diff = abs(min(i1_lower, i2_lower) - max(i1_upper, i2_upper))
child1[i] = max(
min(random.randint(a - diff, b + diff), i1_upper),
i1_lower
)
child2[i] = max(
min(a + b - child1[i], i2_upper),
i2_lower
) # Just in case individuals generated by different spawning pools.
return [child1, child2, ]
PKH`_.
Thanks
======
To you, for using the library, for helping me to make it faster and better, for learn with it and for releasing your
code to make the knowledge and the science open for the rest of humanity.
Warning
=======
It has been developed with only python 3.X in mind, so it won't probably work on lower versions (i.e. no python 2.X
support).
Second warning
==============
English included in both this document and the code can be devastating for the brain of an average human being. Even so
we, the poor developers, are working hard to write as correctly as possible and learn along the way. The documentation
will be updated as we improve our language proficency as well as we receive critical / suggestions for this.
PKH`_.
Thanks
======
To you, for using the library, for helping me to make it faster and better, for learn with it and for releasing your
code to make the knowledge and the science open for the rest of humanity.
Warning
=======
It has been developed with only python 3.X in mind, so it won't probably work on lower versions (i.e. no python 2.X
support).
Second warning
==============
English included in both this document and the code can be devastating for the brain of an average human being. Even so
we, the poor developers, are working hard to write as correctly as possible and learn along the way. The documentation
will be updated as we improve our language proficency as well as we receive critical / suggestions for this.
PKH
:pynetics/ga_list/ga_real.pyPK:H6 Hpynetics/ga_list/ga_int.pyPKH