PKHHGpynetics/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):
""" A random value is selected uniformly over the interval. """
return random.uniform(self.a, self.b)
class PlainRecombination(ListRecombination):
def __call__(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 = super().__call__(parent1, parent2)
for g in range(len(parent1)):
lower_bound = min(parent1[g], parent2[g])
upper_bound = max(parent1[g], parent2[g])
child1[g] = random.uniform(lower_bound, upper_bound)
child2[g] = upper_bound - (child1[g] - lower_bound)
return child1, child2
class FlexibleRecombination(ListRecombination):
def __init__(self, α):
self.α = α
def __call__(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 = super().__call__(parent1, parent2)
for g in range(len(parent1)):
lower_bound = min(parent1[g], parent2[g]) - self.α
upper_bound = max(parent1[g], parent2[g]) + self.α
child1[g] = random.uniform(lower_bound, upper_bound)
child2[g] = upper_bound - (child1[g] - lower_bound)
return child1, child2
class MorphologicalRecombination(ListRecombination):
# TODO We need to improve this crossover (maybe a diversity for population?)
"""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].
"""
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 __call__(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 = super().__call__(parent1, parent2)
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^lHSMpynetics/ga_int.pyimport random
from pynetics import ga_list
from pynetics.ga_list import ListIndividualSpawningPool, \
ListRecombination
class IntegerIndividualSpawningPool(ListIndividualSpawningPool):
""" Defines the methods for creating integer individuals. """
def __init__(self, size, lower, upper, fitness, diversity):
""" 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).
:param fitness: The method to evaluate individuals. It's expected to be
a callable that returns a float value where the higher the value,
the better the individual. Instances of subclasses of class Fitness
can be used for this purpose.
:param diversity: The method to compute the diversity of a sequence of
individuals generated by this SpawningPool instance. Is expected to
be a function that generates a diversity representation given a
subset of individuals. Instances of subclasses of class Diversity
can be used for this purpose.
"""
super().__init__(
size=size,
alleles=ga_list.FiniteSetAlleles(range(lower, upper + 1)),
fitness=fitness,
diversity=diversity,
)
self.lower = lower
self.upper = upper
class IntegerRangeRecombination(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 __call__(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.
"""
child1, child2 = super().__call__(parent1, parent2)
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
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
PK^lHVV
pynetics/selections.pyimport operator
import random
from pynetics import Selection
class BestIndividual(Selection):
""" Selects the best individuals among the population. """
def perform(self, population, n):
""" Gets the top n individuals out of all the population.
If "repetable" is activated, the returned individuals will be n times
the best individual. If False, the returned individuals will be the top
n individuals.
:param population: The population from which select the individuals.
:param n: The number of individuals to return.
:return: A list of n individuals.
"""
return population[:n]
class ProportionalToPosition(Selection):
""" Selects individuals randomly proportionally to their position. """
def perform(self, population, n):
""" Gets randomly the individuals, giving more probability to those in
first positions of the population, i.e. those fittest.
The probability to be selected is proportional to the position of the
fitness of the individual among the population (i.e. those with better
fitness have better positions, but a very high fitness doesn't implies
more chances to be selected).
If "repetable" is activated, the returned individuals may be repeated.
:param n: The number of individuals to return.
:param population: The population from which select the individuals.
:return: A list of n individuals.
"""
# TODO Implement
raise NotImplementedError()
class Tournament(Selection):
""" Selects best individuals of a random sample of the whole population. """
def __init__(self, sample_size):
""" Initializes this selector.
:param sample_size: The size of the random sample of individuals to pick
prior to make the selection of the fittest.
:param repetable: If repetition of individuals is allowed. If true,
there are chances of the same individual be selected again. Defaults
to False.
"""
self.sample_size = sample_size
def perform(self, population, n):
""" Gets the best individuals from a random sample of the population.
To do it, a sample of individuals will be selected randomly and, after
that, the best individual of the sample is then selected. This process
(i.e. extract sample and the get best individual from sample) is done
as many times as individuals to be selected.
If "rep" is activated, the returned individuals may be repeated.
:param n: The number of individuals to return.
:param population: The population from which select the individuals.
:return: A list of n individuals.
"""
individuals = []
for _ in range(n):
sample = random.sample(population, self.sample_size)
individuals.append(
max(sample, key=operator.methodcaller('fitness'))
)
return individuals
class Uniform(Selection):
""" Selects individuals randomly from the population. """
def perform(self, population, n):
""" Selects n individuals randomly from the population.
The selection is done by following a uniform distribution along the
entire population.
:param population: The population from which select the individuals.
:param n: The number of individuals to return.
:return: A list of n individuals.
"""
random.sample(population, n)
PK^lH*s8pynetics/replacements.pyfrom pynetics import Replacement
class LowElitism(Replacement):
""" Low elitism replacement.
The method will replace the less fit individuals by the ones specified in
the offspring. This makes this operator elitist, but at least not much.
Moreover, if offspring size equals to the population size then it's a full
replacement (i.e. a generational scheme).
"""
def __call__(self, population, individuals):
""" Removes less fit individuals and then inserts the offspring.
:param population: The population where make the replacement.
:param individuals: The new population to use as replacement.
"""
if individuals:
population.sort()
del population[-len(individuals):]
population.extend(individuals)
class HighElitism(Replacement):
""" Drops the less fit individuals among all (population plus offspring).
The method will add all the individuals in the offspring to the population,
removing afterwards those individuals less fit. This makes this operator
highly elitist but if length os population and offspring are the same, the
process will result in a full replacement, i.e. a generational scheme of
replacement.
"""
def __call__(self, population, indviduals):
""" Inserts the offspring in the population and removes the less fit.
:param population: The population where make the replacement.
:param indviduals: The new population to use as replacement.
"""
if indviduals:
population.sort()
population.extend(indviduals)
del population[-len(indviduals):]
PKHɁ11pynetics/ga_list.pyimport random
from abc import ABCMeta, abstractmethod
from pynetics import SpawningPool, Individual, Recombination, \
take_chances, Mutation, Diversity
class Alleles(metaclass=ABCMeta):
""" The alleles are all the possible values a gene can take. """
@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, symbols):
""" Initializes this set of alleles with its sequence of symbols.
The duplicated values are removed in the list maintained by this alleles
class, so none of the different symbols has a higher probability to be
selected.
:param symbols: The sequence of symbols.
"""
self.symbols = set(symbols)
def get(self):
""" A random value is selected uniformly over the set of values. """
return random.choice(tuple(self.symbols))
class ListIndividualsWithFiniteSetAllelesDiversity(Diversity):
""" Computes the diversity in a set of BinaryIndividual instances. """
def __call__(self, individuals):
""" Returns a value repersenting the diversity of the individuals.
The value is computed as follows. For each gene position, a value of M
(the number of different appearing alleles) is computed. Then, all the
values are added and the divided by the Y = N * L where L is the length
of the individual and N the number of possible alleles. The value then
is expected to belong to the interval [0, 1], where 0 is no diversity at
all and 1 a completly diverse population.
:param individuals: A sequence of individuals from which obtain the
diversity.
:return: A float value between 0 and 1 with the value of the diversity.
"""
genes_diversity = sum(len(set(x)) for x in zip(*individuals))
total_diversity = len(
individuals[0].population.spawning_pool.alleles.symbols
) * len(individuals[0])
return float(genes_diversity) / float(total_diversity)
class ListIndividualSpawningPool(SpawningPool):
""" Defines the methods for creating individuals required by population. """
def __init__(self, size, alleles, fitness, diversity):
""" 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.
:param fitness: The method to evaluate individuals. It's expected to be
a callable that returns a float value where the higher the value,
the better the individual. Instances of subclasses of class Fitness
can be used for this purpose.
:param diversity: The method to compute the diversity of a sequence of
individuals generated by this SpawningPool instance. Is expected to
be a function that generates a diversity representation given a
subset of individuals. Instances of subclasses of class Diversity
can be used for this purpose.
"""
super().__init__(fitness=fitness, diversity=diversity)
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
# Maybe instead inherit from list is better inherit from mutablesequence
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):
""" A default phenotype for this kind of invdividuals.
:return: A list where each of the elements is the string representation
of each of the genes.
"""
return [str(g) for g in self]
def clone(self):
""" Clones this ListIndividual.
:return: A ListIndividual looking exactly like this.
"""
individual = super().clone()
for gene in self:
individual.append(gene)
return individual
class ListRecombination(Recombination, metaclass=ABCMeta):
""" Behavior for recombinations where lengths should be the same. """
@abstractmethod
def __call__(self, *args):
""" Performs checks over the lengths before executing perform.
Specifically, it checks if the length of all individuals are the same.
In so, the crossover operation is done. If not, a PyneticsError is
raised.
:param args: The individuals to use as parents from which generate
the progeny.
:return: A tuple with cloned parents (same order).
"""
return tuple(i.clone() for i in args)
class OnePointRecombination(ListRecombination):
""" Offspring is created by mixing the parents using one random pivot point.
This recombination implementation works with two (and only two) individuals
of type ListIndividual (or subclasses).
"""
def __call__(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 = super().__call__(parent1, parent2)
p = random.randint(1, len(parent1) - 1)
for i in range(len(parent1)):
if i >= p:
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):
""" 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 = super().__call__(parent1, parent2)
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 not p < i < q:
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).
Louis, S. J. and Rawlins, G. J. E. (1991) Designer Genetic Algorithms:
Genetic Algorithms in Structures Design. In R. K. Belew and L. B. Booker
(eds.) Proceedings of the Fourth International Conferenceon Genetic
Algorithms (San Mateo: Morgan Kau man), 53-60.
"""
def __call__(self, parent1, parent2):
""" 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 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 = super().__call__(parent1, parent2)
for i in range(len(parent1)):
if take_chances(.5):
child1[i], child2[i] = parent2[i], parent1[i]
return child1, child2
class SwapGenes(Mutation):
""" Mutates the by swapping two random genes.
This mutation method operates only with ListIndividuals (or any of their
subclasses.
"""
def __call__(self, individual, p):
""" 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.
:param p: The probability of mutation.
:return: A new individual mutated with a probabiity of p or looking
exactly to the one passed as parameter with a probability of 1-p.
"""
clone = individual.clone()
if take_chances(probability=p):
# Get two random diferent indexes
indexes = range(len(individual))
i1, i2 = tuple(random.sample(indexes, 2))
# Swap the genes in the cloned individual
clone[i1], clone[i2] = clone[i2], clone[i1]
return clone
else:
return clone
class SingleGeneRandomValue(Mutation):
""" Mutates the individual by changing the value to a random gene. """
def __init__(self, alleles):
""" Initializes this object.
:param alleles: The set of values to choose from.
"""
super().__init__()
self.alleles = alleles
def __call__(self, individual, p):
""" 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.
:param p: The probability of mutation.
"""
clone = individual.clone()
if take_chances(probability=p):
# Set in a random position a different gene than before
i = random.choice(range(len(individual)))
new_gene = self.alleles.get()
while individual[i] == new_gene:
new_gene = self.alleles.get()
# Set this gene in the cloned individual
clone[i] = new_gene
return clone
else:
return clone
class NGeneRandomValue(Mutation):
""" Mutates the individual by changing the value to a random gene. """
def __init__(self, alleles, n=None):
""" Initializes this object.
:param alleles: The set of values to choose from.
:param n: The number of how many random genes may mutate. If greater
than the size of individuals or None, the prone genes are all.
Defaults to None.
"""
super().__init__()
self.alleles = alleles
self.n = n
def __call__(self, individual, p):
""" 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.
:param p: The probability of mutation.
"""
indexes = random.sample(
range(len(individual)),
min(self.n or len(individual), len(individual)),
)
clone = individual.clone()
for i in indexes:
if take_chances(probability=p):
new_gene = self.alleles.get()
while individual[i] == new_gene:
new_gene = self.alleles.get()
clone[i] = new_gene
return clone
PK#H ..pynetics/algorithms.pyimport inspect
import math
import multiprocessing
import random
from pynetics import Population, GeneticAlgorithm, take_chances, PyneticsError, \
NoMutation
class SimpleGA(GeneticAlgorithm):
""" Simple implementation of a GeneticAlgorithm
This subclass implements the basic behavior of a genetic algorithm with some
degree of configuration.
"""
def __init__(
self,
stop_condition,
size,
spawning_pool,
selection,
recombination,
replacement,
mutation=None,
p_recombination=0.9,
p_mutation=0.1,
replacement_rate=1.0,
):
""" Initializes this instance.
:param stop_condition: The condition to be met in order to stop the
genetic algorithm.
:param size: The size this population should have.
:param spawning_pool: The object that generates individuals.
:param selection: The method to select individuals of the population to
recombine.
:param replacement: The method that will add and remove individuals from
the population given the set of old individuals (i.e. the ones on
the population before the evolution step) and new individuals (i.e.
the offspring).
:param recombination: The method to recombine parents in order to
generate an offspring with characteristics of the parents. If none,
no recombination will be applied.
:param mutation: The method to mutate an individual. If none, no
mutation over the individual will be applied. If not provided, no
mutation is performed.
:param p_recombination: The odds for recombination method to be
performed over a set of selected individuals to generate progeny. If
not performed, progeny will be the parents. Must be a value between
0 and 1 (both included). If not provided, defaults to 1.0.
:param p_mutation: The odds for mutation method to be performed over a
progeny. It's applied once for each individual. If not performed the
individuals will not be modified. Must be a value between 0 and 1
(both included). If not provided, it defaults to 0.0 (no mutation is
performed).
:param replacement_rate: The rate of individuals to be replaced in each
step of the algorithm. Must be a float value in the (0, 1] interval.
:raises WrongValueForIntervalError: If any of the bounded values fall
out of their respective intervals.
:raises NotAProbabilityError: If a value was expected to be a
probability and it wasn't.
:raises UnexpectedClassError: If any of the input variables doesn't
follow the contract required (i.e. doesn't inherit from a predefined
class).
"""
super().__init__(stop_condition=stop_condition)
self.population_size = size
self.spawning_pool = spawning_pool
self.offspring_size = int(math.ceil(size * replacement_rate))
self.selection = selection
self.recombination = recombination
self.mutation = mutation or NoMutation()
self.replacement = replacement
self.replacement_rate = replacement_rate
self.p_recombination = p_recombination
self.p_mutation = p_mutation
self.selection_size = len(
inspect.signature(recombination.__call__).parameters
)
self.population = None
self.best_individuals = []
def initialize(self):
self.population = Population(
size=self.population_size,
spawning_pool=self.spawning_pool,
)
def step(self):
offspring = []
while len(offspring) < self.offspring_size:
# Selection
parents = self.selection(self.population, self.selection_size)
# Recombination
if take_chances(self.p_recombination):
progeny = self.recombination(*parents)
else:
progeny = [i.clone() for i in parents]
# Mutation
individuals_who_fit = min(
len(progeny),
self.offspring_size - len(offspring)
)
progeny = [
self.mutation(individual, self.p_mutation)
for individual in random.sample(progeny, individuals_who_fit)
]
# Add progeny to the offspring
offspring.extend(progeny)
# Once offspring is generated, a replace step is performed
self.replacement(self.population, offspring)
# We store the best individual for further information
if self.generation < len(self.best_individuals):
self.best_individuals[self.generation] = self.population.best()
else:
self.best_individuals.append(self.population.best())
def best(self, generation=None):
if self.best_individuals:
generation = generation or -1
if generation > len(self.best_individuals) - 1:
raise PyneticsError()
else:
return self.best_individuals[generation]
else:
return None
class ConcurrentGA(GeneticAlgorithm):
def __init__(
self,
stop_condition,
size,
spawning_pool,
selection,
recombination,
replacement,
mutation=None,
p_recombination=0.9,
p_mutation=0.1,
replacement_rate=1.0,
processes=None
):
super().__init__(stop_condition=stop_condition)
self.spawning_pool = spawning_pool
self.offspring_size = int(math.ceil(size * replacement_rate))
self.selection = selection
self.recombination = recombination
self.mutation = mutation or NoMutation()
self.replacement = replacement
self.replacement_rate = replacement_rate
self.p_recombination = p_recombination
self.p_mutation = p_mutation
self.nproc = processes or multiprocessing.cpu_count()
print('Entrenando en {} procesos'.format(self.nproc))
# Round the population size to the next multiple of the # of processes
self.psize = int((size + self.nproc - 1) // self.nproc * self.nproc)
if size != self.psize:
print('Ajustada población a {} para los {} procesos'.format(
self.psize,
self.nproc,
))
# Population will be splited in chunks of psize / nproc
self.csize = int(self.psize / self.nproc)
self.selection_size = len(
inspect.signature(recombination.__call__).parameters
)
self.population = None
self.best_individuals = []
self.evolver_processes = []
class EvolverProcess:
def __init__(
self,
steps,
selection,
recombination,
mutation,
replacement,
replacement_rate,
p_recombination,
p_mutation,
):
self.steps = steps
self.selection = selection
self.recombination = recombination
self.mutation = mutation
self.replacement = replacement
self.replacement_rate = replacement_rate
self.p_recombination = p_recombination
self.p_mutation = p_mutation
self.selection_size = len(
inspect.signature(recombination.__call__).parameters
)
self.input = multiprocessing.Queue()
self.output = multiprocessing.Queue()
self.stop = False
def run(self):
while not self.stop:
population = self.input.get()
offspring_size = int(
math.ceil(len(population) * self.replacement_rate)
)
for _ in range(self.steps):
self.step(population, offspring_size)
offspring = []
for individual in population:
individual.population = None
offspring.append(individual)
self.output.put(offspring)
def step(self, population, offspring_size):
offspring = []
while len(offspring) < offspring_size:
# Selection
parents = self.selection(population, self.selection_size)
# Recombination
if take_chances(self.p_recombination):
progeny = self.recombination(*parents)
else:
progeny = parents
# Mutation
individuals_who_fit = min(
len(progeny),
offspring_size - len(offspring)
)
progeny = [
self.mutation(individual, self.p_mutation)
for individual in
random.sample(progeny, individuals_who_fit)
]
# Add progeny to the offspring
offspring.extend(progeny)
# Once offspring is generated, a replace step is performed
self.replacement(population, offspring)
def initialize(self):
self.population = Population(
size=self.psize,
spawning_pool=self.spawning_pool,
)
self.best_individuals = [self.population.best()]
for i in range(self.nproc):
self.evolver_processes.append(ConcurrentGA.EvolverProcess(
steps=10,
selection=self.selection,
recombination=self.recombination,
mutation=self.mutation,
replacement=self.replacement,
replacement_rate=self.replacement_rate,
p_recombination=self.p_recombination,
p_mutation=self.p_mutation,
))
proc = multiprocessing.Process(target=self.evolver_processes[i].run)
proc.daemon = True
proc.start()
def step(self):
# Reshuffle the population to send the more diversity to the processes
random.shuffle(self.population)
# Send the population chunks to the processes and let them EVOOOOLVE!!!!
for i, evolver_process in enumerate(self.evolver_processes):
# We send to the ith process the ith partition of the population
evolver_process.input.put(Population(
size=self.psize,
spawning_pool=self.spawning_pool,
individuals=self.population[self.csize * i:self.csize * (i + 1)]
), False)
# Recover the resulting individuals
offspring = []
for evolver_process in self.evolver_processes:
offspring += evolver_process.output.get()
# The list should have a maximum size. If greater, we get a sample of it
if len(offspring) > self.offspring_size:
offspring = random.sample(offspring, self.offspring_size)
# Replace the individuals in the population with the new ones
self.replacement(self.population, offspring)
# We store the best individual for further information
if self.generation < len(self.best_individuals):
self.best_individuals[self.generation] = self.population.best()
else:
self.best_individuals.append(self.population.best())
def finish(self):
for evolver_process in self.evolver_processes:
evolver_process.stop = True
multiprocessing.active_children()
def best(self, generation=None):
generation = generation or -1
if generation > len(self.best_individuals) - 1:
raise PyneticsError()
else:
return self.best_individuals[generation]
PK,YlH|?pynetics/utils.pyimport random
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
def clone_empty(obj):
""" Used by classes which need to be cloned avoiding the call to __init__.
:param obj: The object to be cloned.
:return: A newly empty object of the class obj.
"""
class Empty(obj.__class__):
def __init__(self): pass
empty = Empty()
empty.__class__ = obj.__class__
return empty
PK,YlHpynetics/stop.pyfrom pynetics import StopCondition
class StepsNum(StopCondition):
""" If the genetic algorithm has made enough iterations. """
def __init__(self, steps):
""" Initializes this function with the number of iterations.
:param steps: The number of iterations to do before stop.
"""
self.steps = steps
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 than 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.
"""
return genetic_algorithm.generation >= self.steps
class FitnessBound(StopCondition):
""" If the genetic algorithm obtained a fine enough individual. """
def __init__(self, fitness_bound):
""" Initializes this function with the upper bound for the fitness.
:param fitness_bound: A fitness value. The criteria will be met when the
fitness in the algorithm (in one or all populations managed, see
below) is greater than this specified fitness.
"""
self.fitness_bound = fitness_bound
def __call__(self, genetic_algorithm):
""" Checks if this stop criteria is met.
It will look at the fitness of the best individual the genetic algorithm
has discovered. In case of its fitness being greater or equal than the
specified at initialization time, the condition will be met and the
algorithm will stop.
:param genetic_algorithm: The genetic algorithm where this stop
condition belongs.
:return: True if criteria is met, false otherwise.
"""
return genetic_algorithm.best().fitness() >= self.fitness_bound
PKHHGuw&AApynetics/__init__.pyimport operator
import random
from abc import ABCMeta, abstractmethod
from collections import abc
from pynetics.utils import take_chances, clone_empty
from .exceptions import WrongValueForInterval, NotAProbabilityError, \
PyneticsError, InvalidSize
__version__ = '0.4.0'
class GeneticAlgorithm(metaclass=ABCMeta):
""" Base class with the definition of how a GA works.
More than one algorithm may exist so a base class is created for specify the
contract required by the other classes to work properly.
"""
class GAListener:
""" Listener for the events caused by the genetic algorithm. """
@abstractmethod
def algorithm_started(self, ga):
""" Called when the algorithm start.
This method will be called AFTER initialization but BEFORE the first
iteration, including the check against the stop condition.
:param ga: The GeneticAlgorithm instanced that called this method.
"""
@abstractmethod
def algorithm_finished(self, ga):
""" Called when the algorithm finishes.
Particularly, this method will be called AFTER the stop condition
has been met.
:param ga: The GeneticAlgorithm instanced that called this method.
"""
@abstractmethod
def step_started(self, ga):
""" Called when a new step of the genetic algorithm starts.
This method will be called AFTER the stop condition has been checked
and proved to be false) and BEFORE the new step is computed.
:param ga: The GeneticAlgorithm instanced that called this method.
"""
@abstractmethod
def step_finished(self, ga):
""" Called when a new step of the genetic algorithm finishes.
This method will be called AFTER an step of the algorithm has been
computed and BEFORE a new check against the stop condition is going
to be made.
:param ga: The GeneticAlgorithm instanced that called this method.
"""
def __init__(self, stop_condition):
self.stop_condition = stop_condition
self.generation = 0
self.listeners = []
def run(self):
""" Runs the simulation.
The process is as follows: initialize populations and, while the stop
condition is not met, do a new evolve step. This process relies in the
abstract method "step".
"""
self.initialize()
[l.algorithm_started(self) for l in self.listeners]
while self.best() is None or not self.stop_condition(self):
[l.step_started(self) for l in self.listeners]
self.step()
self.generation += 1
[l.step_finished(self) for l in self.listeners]
self.finish()
[l.algorithm_finished(self) for l in self.listeners]
def initialize(self):
""" Called when starting the genetic algorithm to initialize it. """
self.generation = 0
def step(self):
""" Called on every iteration of the algorithm. """
pass
def finish(self):
""" Called one the algorithm has finished. """
pass
@abstractmethod
def best(self, generation=None):
""" Returns the best individual obtained until this moment.
:param generation: The generation of the individual that we want to
recover. If not set, this will be the one emerged in the last
generation. Defaults to None (not set, thus last generation).
:return: The best individual generated in the specified generation.
"""
class StopCondition(metaclass=ABCMeta):
""" A condition to be met in order to stop the algorithm.
Although the stop condition is defined as a class, it's enough to provide a
function that is able to discern whether the time has come to stop (True or
False) receiving as parameter the population.
"""
@abstractmethod
def __call__(self, genetic_algorithm):
""" Checks if this stop condition is met.
:param genetic_algorithm: The genetic algorithm where this stop
condition belongs.
:return: True if criteria is met, false otherwise.
"""
class Individual(metaclass=ABCMeta):
""" 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.
An individual contains a cache for the fitness method that prevents to
compute it over and over again. However, as well as it is possible to
clear this cache, also it is possible to disable it.
:param disable_cache: Disables the fitness cache. Defaults to True,
which means the cache is enabled.
"""
self.population = None
self.fitness_method = None
def fitness(self):
""" Computes the fitness of this individual.
It will use the fitness method defined on its spawning pool.
:param init: If this call to fitness is in initialization time. It
defaults to False.
:return: A float value.
"""
return self.fitness_method(self)
@abstractmethod
def phenotype(self):
""" The expression of this particular individual in the environment.
:return: An object representing this individual in the environment
"""
@abstractmethod
def clone(self):
""" Creates an instance as an exact copy of this individual
If the implementing subclass has internal attributes to be cloned, the
attributes copy should be implemented in an overriden version of this
method.
:return: A brand new individual like this one.
"""
individual = clone_empty(self)
individual.population = self.population
individual.fitness_method = self.fitness_method
return individual
class Diversity:
""" Represents the diversity of a population subset. """
@abstractmethod
def __call__(self, individuals):
""" It returns a value that symbolizes how diverse is the population.
The expected value will rely completely over the Individual
implementation.
:param individuals: A sequence of individuals from which obtain the
diversity.
:return: A value representing the diversity.
"""
class SpawningPool(metaclass=ABCMeta):
""" Defines the methods for creating individuals required by population. """
def __init__(self, fitness, diversity):
""" Initializes this spawning pool.
:param fitness: The method to evaluate individuals. It's expected to be
a callable that returns a float value where the higher the value,
the better the individual. Instances of subclasses of class Fitness
can be used for this purpose.
:param diversity: The method to compute the diversity of a sequence of
individuals generated by this SpawningPool instance. Is expected to
be a function that generates a diversity representation given a
subset of individuals. Instances of subclasses of class Diversity
can be used for this purpose.
"""
self.population = None
self.fitness = fitness
self.diversity = diversity
def spawn(self):
""" Returns a new random individual.
It uses the abstract method "create" to be implemented with the logic
of individual creation. The purpose of this method is to add the
parameters the base individual needs.
:return: An individual instance.
"""
individual = self.create()
individual.population = self.population
individual.fitness_method = self.fitness
return individual
@abstractmethod
def create(self):
""" Creates a new individual randomly.
:return: A new Individual object.
"""
class Population(abc.MutableSequence):
""" Manages a population of individuals.
A population is where individuals of the same kind evolve over an
environment. A basic genetic algorithm consists in a single population, but
more complex schemes involve two or more populations evolving concurrently.
"""
def __init__(self, size=None, spawning_pool=None, individuals=None):
""" Initializes the population, filling it with individuals.
:param size: The size this population should have.
:param spawning_pool: The object that generates individuals.
:param individuals: The list of starting individuals. If none or if its
length is lower than the population size, the rest of individuals
will be generated randomly. If the length of initial individuals is
greater than the population size, a random sample of the individuals
is selected as members of population.
:raises InvalidSize: If the provided size for the population is invalid.
:raises UnexpectedClassError: If any of the instances provided wasn't
of the required class.
"""
if size is None or size < 1:
raise InvalidSize('> 0', size)
self.size = size
self.spawning_pool = spawning_pool
self.spawning_pool.population = self
if individuals is not None:
self.individuals = [i.clone() for i in individuals]
else:
self.individuals = []
while len(self.individuals) > self.size:
self.individuals.remove(random.choice(self.individuals))
while len(self.individuals) < self.size:
self.individuals.append(self.spawning_pool.spawn())
self.__sorted = False
self.__diversity = None
def diversity(self):
if self.__diversity is None:
self.__diversity = self.spawning_pool.diversity(self.individuals)
return self.__diversity
def sort(self):
""" Sorts this population from best to worst individual. """
if self.__sorted is False:
self.individuals.sort(
key=operator.methodcaller('fitness'),
reverse=True
)
self.__sorted = True
def __len__(self):
""" Returns the number fo individuals this population has. """
return len(self.individuals)
def __delitem__(self, i):
""" Removes the ith individual from the population.
The population will be sorted by its fitness before deleting.
:param i: The ith individual to delete.
"""
del self.individuals[i]
self.__diversity = None
def __setitem__(self, i, individual):
""" Puts the named individual in the ith position.
This call will cause a new sorting of the individuals the next time an
access is required. This means that is preferable to make all the
inserts in the population at once instead doing interleaved readings and
inserts.
:param i: The position where to insert the individual.
:param individual: The individual to be inserted.
"""
individual.population = self
self.individuals.__setitem__(i, individual)
self.__sorted = False
self.__diversity = None
def insert(self, i, individual):
""" Ads a new element to the ith position of the population population.
This call will cause a new sorting of the individuals the next time an
access is required. This means that is preferable to make all the
inserts in the population at once instead doing interleaved readings and
inserts.
:param i: The position where insert the individual.
:param individual: The individual to be inserted in the population
"""
individual.population = self
self.individuals.insert(i, individual)
self.__sorted = False
self.__diversity = None
def __getitem__(self, i):
""" Returns the individual located on the ith position.
The population will be sorted before accessing to the element so it's
correct to assume that the individuals are arranged from fittest (i = 0)
to least fit (n len(populaton)).
:param i: The index of the individual to retrieve.
:return: The individual.
"""
return self.individuals.__getitem__(i)
def best(self):
""" Returns the best individual for the gth.
:return: The best individual for that generation.
"""
self.sort()
return self[0]
class Fitness(metaclass=ABCMeta):
""" Method to estimate how adapted is the individual to the environment. """
@abstractmethod
def __call__(self, individual):
""" Estimates how adapted is the individual.
Must return a float value where the higher the value, the better the
adaption of the individual to the environment.
:param individual: The individual to which estimate the adaptation.
:return: A float value pointing the adation to the environment.
"""
class Mutation(metaclass=ABCMeta):
""" Defines the behaviour of a genetic algorithm mutation operator. """
@abstractmethod
def __call__(self, individual, p):
""" Applies the mutation method to the individual.
:param individual: an individual to mutate.
:param p: The probability of mutation.
:return: A cloned individual of the one passed as parameter but with a
slightly (or not, X-MEN!!!!) mutation.
"""
class NoMutation(Mutation):
def __call__(self, individual, p):
return individual
class Recombination(metaclass=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.
"""
@abstractmethod
def __call__(self, *args):
""" Implementation of the recombine method.
:param args: One or more Individual instances to use as parents in the
recombination.
:return: A sequence of individuals with characteristics of the parents.
"""
class Replacement(metaclass=ABCMeta):
""" Replacement of individuals of the population. """
@abstractmethod
def __call__(self, population, individuals):
""" It makes the replacement according to the subclass implementation.
:param population: The population where make the replacement.
:param individuals: The new population to use as replacement.
"""
class Selection(metaclass=ABCMeta):
""" Selection of the fittest individuals among the population.
The selection method is defined as a class. However, it is enough to provide
as a selection method, i.e. a function that receives a sequence and a number
of individuals, and returns a sample of individuals of that size from the
given population.
"""
def __call__(self, population, n):
""" Makes some checks to the configuration before delegating selection.
After checking the parameters, the selection is performed by perform
method.
:param population: The population from which select the individuals.
:param n: The number of individuals to return.
:return: A sequence of individuals.
:raises PyneticsError: If length of the population is smaller than the
number of individuals to select and the repetition parameter is set
to False (i.e. the same Individual cannot be selected twice or more
times).
"""
if len(population) < n:
raise PyneticsError()
else:
return self.perform(population, n)
@abstractmethod
def perform(self, population, n):
""" It makes the selection according to the subclass implementation.
:param population: The population from which select the individuals.
:param n: The number of individuals to return.
:return: A sequence of n individuals.
"""
class Catastrophe(metaclass=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.
"""
@abstractmethod
def __call__(self, population):
""" Applies the catastrophe to the specified population.
:param population: The population where apply the catastrophic method.
"""
PK]mHYaapynetics/ga_bin.pyfrom array import array
import random
from collections import abc
from pynetics import Individual, SpawningPool, Mutation, take_chances, Diversity
from pynetics.ga_list import ListRecombination
class BinaryIndividualSpawningPool(SpawningPool):
""" Defines the methods for creating binary individuals. """
def __init__(self, size, fitness, diversity):
""" Initializes this spawning pool for generating binary individuals.
:param size: The size of the individuals to be created from
this spawning pool.
:param fitness: The method to evaluate individuals. It's expected to be
a callable that returns a float value where the higher the value,
the better the individual. Instances of subclasses of class Fitness
can be used for this purpose.
:param diversity: The method to compute the diversity of a sequence of
individuals generated by this SpawningPool instance. Is expected to
be a function that generates a diversity representation given a
subset of individuals. Instances of subclasses of class Diversity
can be used for this purpose.
"""
super().__init__(
fitness=fitness,
diversity=diversity
)
self.individual_size = size
def create(self):
individual = BinaryIndividual()
individual.genes = array('B', [
random.getrandbits(1)
for _ in range(self.individual_size)
])
return individual
class MomentOfInertia(Diversity):
""" A diversity implementation based on centroids and inertia.
Extracted from paper "Measurement of Population Diversity" of R.W. Morrison
et. al.
"""
# TODO Not working. Review.
def __call__(self, individuals):
centroid_vector = [0 for _ in individuals[0]]
for individual in individuals:
for i, gene in enumerate(individual):
centroid_vector[i] += gene
for i, centroid in enumerate(centroid_vector):
centroid_vector[i] = centroid / len(individuals)
diversity = 0
for i in range(len(individuals[0])):
for j, individual in enumerate(individuals):
diversity += (individual[i] - centroid_vector[i]) ** 2
return diversity
class AverageHamming(Diversity):
""" Diversity implementation of the average of each hamming loci distances.
Predicting Convergence Time for Genetic Algorithms Sushil J. Louis and
Gregory J. E. Rawlins
"""
def __call__(self, individuals):
diversity = 0.0
total = 0.0
individual_len = len(individuals[0])
for j, i1 in enumerate(individuals[:-1]):
for k, i2 in enumerate(individuals[j:]):
diversity += self.f(i1, i2)
total += individual_len
return diversity / total
@staticmethod
def f(i1, i2):
hamming = 0.0
for g1, g2 in zip(i1, i2):
if g1 != g2:
hamming += 1
return hamming
class BinaryIndividual(Individual, abc.MutableSequence):
""" An individual represented by a binary chromosome. """
def __init__(self):
super().__init__()
self.genes = None
def __getitem__(self, index):
return self.genes[index]
def __delitem__(self, index):
return self.genes.remove(index)
def insert(self, index, value):
self.genes.insert(index, value)
def __setitem__(self, index, value):
self.genes[index] = value
def __len__(self):
return len(self.genes)
def phenotype(self):
return self.genes
def clone(self):
clone = super().clone()
clone.genes = self.genes[:]
return clone
class GeneralizedRecombination(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)
"""
def __call__(self, parent1, parent2):
""" Applies the crossover operator.
:param parent1: One of the individuals from which generate the progeny.
:param parent2: The other.
:return: A tuple with the two children for this parents.
"""
child1, child2 = super().__call__(parent1, parent2)
# Obtain the crossover range (as integer values)
a = int(''.join([str(b1 & b2) for (b1, b2) in zip(child1, child2)]), 2)
b = int(''.join([str(b1 | b2) for (b1, b2) in zip(child1, child2)]), 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_formatter = '{:#0' + str(len(child1) + 2) + 'b}'
bin_c = [int(x) for x in bin_formatter.format(c)[2:]]
bin_d = [int(x) for x in bin_formatter.format(d)[2:]]
# Convert to chromosomes and we're finish
for i in range(len(bin_c)):
child1[i], child2[i] = bin_c[i], bin_d[i]
return child1, child2
class AllGenesCanSwitch(Mutation):
def __call__(self, individual, p):
""" Returns the same instance of the individual mutated.
:param individual: The individual to mutate.
:param p: The probability for a gene to mutate.
:return: The same instance maybe mutated).
"""
for i, gene_value in enumerate(individual):
if take_chances(probability=p):
individual[i] = 1 - gene_value
return individual
PK^lH\ݵpynetics/catastrophe.pyimport abc
# TODO Refactor these inefficient methods
from pynetics import Catastrophe
from .utils import take_chances
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 __call__(self, population):
if take_chances(self.__probability):
self.perform(population)
@abc.abstractmethod
def perform(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(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.spawning_pool.spawn()
visited_individuals.append(population[i])
class DoomsdayByProbability(ProbabilityBasedCatastrophe):
""" Replaces all but the best individual. """
def perform(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.spawn()
PK,YlHdpynetics/exceptions.pyclass PyneticsError(Exception):
""" Base class for the errors raised in pynetics library. """
pass
class InvalidSize(PyneticsError):
""" Raised when an instance is not of the expected class. """
def __init__(self, expected, current):
""" Initializes the exception.
:param expected: The expected size.
:param current: The current size.
"""
super().__init__('Expected {} but got {}'.format(expected, current))
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,YlHC'PPpynetics/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(list)
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.
:param initial_symbol: The start symbol from which to generate the tree.
"""
# 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,YlHKQQ"pynetics/gggp/test_gggp_grammar.pyfrom unittest import TestCase
from pynetics.gggp import grammar
class SingleTermTestCase(TestCase):
""" Test for SingleTerm instances. """
def test_value_is_not_a_string(self):
""" Param "value" should be a string. """
for invalid_value in ([], (), {}, None, grammar.SingleTerm('hello')):
with self.assertRaises(ValueError):
grammar.SingleTerm(invalid_value)
def test_different_instances_with_same_content_are_equal(self):
""" If two different instances has the same content are equal. """
self.assertEquals(
grammar.SingleTerm('t'),
grammar.SingleTerm('t'),
)
def test_correct_value_as_a_string(self):
""" If value is a string, the term initializes correctly. """
string_value = 'terminal'
self.assertEquals(string_value, grammar.SingleTerm(string_value).value)
class AndTestCase(TestCase):
""" Test for And connector instances. """
def test_value_is_not_a_tuple(self):
""" If value is not a tuple, an error is raised. """
with self.assertRaises(ValueError):
grammar.And(grammar.SingleTerm('terminal'))
def test_value_is_a_tuple_of_less_than_two_terms(self):
""" If value is a tuple of less than 2 terms, an error is raised. """
invalid_values = (
tuple(),
grammar.SingleTerm('terminal'),
)
for value in invalid_values:
with self.assertRaises(ValueError):
grammar.And(value)
def test_not_all_values_in_value_tuple_are_terms_or_strings(self):
""" If elements in value are not Term, an error is raised. """
invalid_values = (
(grammar.SingleTerm('terminal'), 'a nice dog', 9),
(1, 2, 3, 4, 5),
)
for value in invalid_values:
with self.assertRaises(ValueError):
grammar.And(value)
def test_different_instances_with_same_content_are_equal(self):
""" If two different instances has the same content are equal. """
value = (
grammar.SingleTerm('terminal1'),
'terminal2'
)
self.assertEquals(
grammar.And(*value),
grammar.And(*value),
)
def test_correct_construction(self):
""" If value is correct, the term is correctly initialized. """
value = (
grammar.SingleTerm('terminal1'),
'terminal2'
)
and_connector = grammar.And(*value)
self.assertEquals(value[0], and_connector.value[0])
self.assertEquals(grammar.SingleTerm(value[1]), and_connector.value[1])
class OrTestCase(TestCase):
""" Test for Or connector instances. """
def test_value_is_not_a_tuple(self):
""" If value is not a tuple, it raises an error. """
with self.assertRaises(ValueError):
grammar.Or(grammar.SingleTerm('terminal'))
def test_value_is_a_tuple_of_less_than_two_terms(self):
""" If value is a tuple of less than 2 terms, it raises an error. """
invalid_values = (
tuple(),
grammar.SingleTerm('terminal'),
)
for value in invalid_values:
with self.assertRaises(ValueError):
grammar.Or(value)
def test_not_all_values_in_value_tuple_are_not_term_string_or_tuple(self):
""" If value is a tuple of less than 2 terms, it raises an error. """
invalid_values = (
(grammar.SingleTerm('terminal'), 'a nice dog', 9),
(1, 2, 3, 4, 5),
)
for value in invalid_values:
with self.assertRaises(ValueError):
grammar.Or(value)
def test_if_values_in_value_are_tuples_not_all_weights_are_numbers(self):
""" If value is a tuple of less than 2 terms, it raises an error. """
with self.assertRaises(ValueError):
grammar.Or((
(grammar.SingleTerm('terminal'), 2),
(grammar.SingleTerm('terminal'), '8'),
))
def test_correct_construction_with_weights(self):
""" If value correct with weights, term is correctly initialized. """
value_and_weights = (
(grammar.SingleTerm('t1'), 1),
('t2', 3),
)
value = tuple(t[0] for t in value_and_weights)
weights = tuple(t[1] for t in value_and_weights)
or_connector = grammar.Or(*value_and_weights)
self.assertEquals(value[0], or_connector.value[0])
self.assertEquals(grammar.SingleTerm(value[1]), or_connector.value[1])
self.assertEquals(weights, or_connector.weights)
def test_different_instances_with_same_content_are_equal(self):
""" If two different instances has the same content are equal. """
value = (
grammar.SingleTerm('terminal1'),
'terminal2'
)
self.assertEquals(
grammar.Or(*value),
grammar.Or(*value),
)
def test_correct_construction_without_weights(self):
""" If value correct without weights, term is correctly initialized. """
value = (
grammar.SingleTerm('t1'),
't2',
)
or_connector = grammar.Or(*value)
self.assertEquals(value[0], or_connector.value[0])
self.assertEquals(grammar.SingleTerm(value[1]), or_connector.value[1])
self.assertEquals(tuple(1 for _ in value), or_connector.weights)
class MultiplierTestCase(TestCase):
""" Test for Multiplier instances. """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__default_lower = 0
self.__default_upper = float('Inf')
self.__default_p = 0.5
def test_value_is_not_a_term_or_is_a_multiple_term(self):
""" Param value should be a term other than Multiplier instance. """
for invalid_value in (
[],
(),
{},
None,
grammar.Multiplier(grammar.SingleTerm('t')),
):
with self.assertRaises(ValueError):
grammar.Multiplier(invalid_value)
def test_p_should_be_an_int_or_float(self):
""" Value p should be a number between 0.0 and 1.0 both incl. """
for invalid_p in (
[],
(),
{},
None,
'7',
-0.1,
1.1,
):
with self.assertRaises(ValueError):
grammar.Multiplier(grammar.SingleTerm('t'), p=invalid_p)
def test_different_instances_with_same_content_are_equal(self):
""" If two different instances has the same content are equal. """
self.assertEquals(
grammar.Multiplier('t'),
grammar.Multiplier('t'),
)
def test_correct_construction_with_string_without_anything(self):
""" Correct construction without optional params and a string term.
That means that "lower", "upper" and "p" take the correct values.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
multiplier = grammar.Multiplier(str_value)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_string_with_only_lower(self):
""" Correct construction with only "lower" param and a string term.
That means that "upper" and "p" take the correct values.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
lower = 1
multiplier = grammar.Multiplier(str_value, lower=lower)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_string_with_only_upper(self):
""" Correct construction with only "upper" param and a string term.
That means that "lower" and "p" take the correct values.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
upper = 1
multiplier = grammar.Multiplier(str_value, upper=upper)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_string_with_only_p(self):
""" Correct construction with only "p" param and a string term.
That means that "lower" and "upper" take the correct values.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
p = 0.2
multiplier = grammar.Multiplier(str_value, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_string_with_lower_and_upper(self):
""" Correct construction with only "lower" and "upper" param and a
string term.
That means that "p" takes the correct value.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
lower = 3
upper = 5
multiplier = grammar.Multiplier(str_value, lower=lower, upper=upper)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_string_with_lower_and_p(self):
""" Correct construction with only "lower" and "p" param and a
string term.
That means that "upper" takes the correct value.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
lower = 7
p = 0.75
multiplier = grammar.Multiplier(str_value, lower=lower, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_string_with_upper_and_p(self):
""" Correct construction with only "upper" and "p" param and a
string term.
That means that "lower" takes the correct value.
"""
str_value = 't'
value = grammar.SingleTerm(str_value)
upper = 7
p = 0.75
multiplier = grammar.Multiplier(str_value, upper=upper, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_string_with_lower_upper_and_p(self):
""" Correct construction with all optional params and a string term. """
str_value = 't'
value = grammar.SingleTerm(str_value)
lower = 1
upper = 7
p = 0.75
multiplier = grammar.Multiplier(
str_value,
lower=lower,
upper=upper,
p=p
)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_term_without_anything(self):
""" Correct construction without optional params and a term instance.
That means that "lower", "upper" and "p" take the correct values.
"""
value = grammar.SingleTerm('t')
multiplier = grammar.Multiplier(value)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_term_with_only_lower(self):
""" Correct construction with only "lower" param and a term instance.
That means that "upper" and "p" take the correct values.
"""
value = grammar.SingleTerm('t')
lower = 1
multiplier = grammar.Multiplier(value, lower=lower)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_term_with_only_upper(self):
""" Correct construction with only "upper" param and a term instance.
That means that "lower" and "p" take the correct values.
"""
value = grammar.SingleTerm('t')
upper = 1
multiplier = grammar.Multiplier(value, upper=upper)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_term_with_only_p(self):
""" Correct construction with only "p" param and a term instance.
That means that "lower" and "upper" take the correct values.
"""
value = grammar.SingleTerm('t')
p = 0.2
multiplier = grammar.Multiplier(value, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_term_with_lower_and_upper(self):
""" Correct construction with only "lower" and "upper" params and a
term instance.
That means that "p" takes the correct value.
"""
value = grammar.SingleTerm('t')
lower = 3
upper = 5
multiplier = grammar.Multiplier(value, lower=lower, upper=upper)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(self.__default_p, multiplier.p)
def test_correct_construction_with_term_with_lower_and_p(self):
""" Correct construction with only "lower" and "p" params and a
term instance.
That means that "upper" takes the correct value.
"""
value = grammar.SingleTerm('t')
lower = 7
p = 0.75
multiplier = grammar.Multiplier(value, lower=lower, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(self.__default_upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_term_with_upper_and_p(self):
""" Correct construction with only "upper" and "p" params and a
term instance.
That means that "lower" takes the correct value.
"""
value = grammar.SingleTerm('t')
upper = 7
p = 0.75
multiplier = grammar.Multiplier(value, upper=upper, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(self.__default_lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
def test_correct_construction_with_term_with_lower_upper_and_p(self):
""" Correct construction with all params and a term instance. """
value = grammar.SingleTerm('t')
lower = 1
upper = 7
p = 0.75
multiplier = grammar.Multiplier(value, lower=lower, upper=upper, p=p)
self.assertEquals(value, multiplier.value)
self.assertEquals(lower, multiplier.lower)
self.assertEquals(upper, multiplier.upper)
self.assertAlmostEqual(p, multiplier.p)
class TestProduction(TestCase):
""" Tests the production rules work properly. """
def test_init_fails_if_term_is_not_a_term_instance(self):
""" Beta should be a list and not a simple object. """
variable = 'variable'
for invalid_term in (
[grammar.EpsilonTerminal, ],
(grammar.EpsilonTerminal,),
{'terminal': grammar.EpsilonTerminal},
None,
'7',
42,
):
with self.assertRaises(ValueError):
grammar.Production(variable, invalid_term)
def test_correct_construction(self):
""" Tests alpha is assigned to a read_only property. """
variable = 'non-terminal'
production = grammar.Production(variable, grammar.EpsilonTerminal)
self.assertEquals(variable, production.variable)
self.assertEquals(grammar.EpsilonTerminal, production.term)
class GrammarTestCase(TestCase):
""" Tests for Grammar instances. """
def test_start_symbol_should_exists_as_variable_in_productions(self):
""" The start symbol must lead at least one of the productions. """
existent_start_symbol = 'existent'
non_existent_start_symbol = 'non_existent'
productions = [
grammar.Production(
existent_start_symbol,
grammar.EpsilonTerminal
) for _ in range(4)
]
# Test OK if start_symbol exists.
grammar.Grammar(
start_symbol=existent_start_symbol,
productions=productions
)
# Test KO if start_symbol_does_not_exists
with self.assertRaises(ValueError):
grammar.Grammar(
start_symbol=non_existent_start_symbol,
productions=productions
)
def test_productions_should_be_an_list_of_production_instances(self):
""" Productions should be a list of Production instances. """
all_but_one = [
grammar.Production(
't',
grammar.EpsilonTerminal
) for _ in range(4)
]
all_but_one.append(grammar.EpsilonTerminal)
for invalid_productions in (
(),
{},
None,
'terminal',
['terminal'],
grammar.EpsilonTerminal,
[grammar.EpsilonTerminal],
all_but_one,
):
with self.assertRaises(ValueError):
grammar.Grammar('terminal', invalid_productions)
def test_correct_construction(self):
""" Correct start_symbol and productions lead to a correct grammar. """
base_symbol = 't'
start_symbol = base_symbol + '0'
productions = [
grammar.Production(
base_symbol + str(i),
grammar.EpsilonTerminal
) for i in range(4)
]
a_grammar = grammar.Grammar(
start_symbol=start_symbol,
productions=productions
)
self.assertEquals(start_symbol, a_grammar.start_symbol)
for production in productions:
self.assertIn(production, a_grammar.productions)
for production in a_grammar.productions:
self.assertIn(production, productions)
def test_repeated_productions_are_shrunk(self):
""" If two or more productions has the same alpha, they're shrunk.
This implies that if two or more productions has the same alpha, they're
transformed in a single production with all the beta parts of the
productions separated by Or's.
"""
start_symbol = 'expr'
productions = [
grammar.Production(
'expr',
grammar.SingleTerm(str(i))
) for i in range(10)
]
a_grammar = grammar.Grammar(start_symbol, productions)
# The ten productions are transformed in One
self.assertEquals(len(a_grammar.productions), 1)
# The only term of the productions is an Or instance
production = a_grammar.productions[0]
self.assertEquals(start_symbol, production.variable)
self.assertIsInstance(production.term, grammar.Or)
# The Or term contains all the terms in initial productions.
value = production.term.value
for t in value:
self.assertIn(t, [p.term for p in productions])
for t in [p.term for p in productions]:
self.assertIn(t, value)
# TODO Test grammar.variables
# TODO Test grammar.terminals
# TODO Test Term.random_derivation (all of the terms)
# TODO Test DerivationTree
PK,YlH˶4MMpynetics/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,YlHQmwpynetics/gggp/__init__.py__author__ = 'blazaid'
PKHHC"AA(pynetics-0.4.0.dist-info/DESCRIPTION.rst==========
Pyvolution
==========
***********************************************
An evolutionary computation library for Python.
***********************************************
Pyvolution is a library for experimenting with evolutionary computation.
************
Installation
************
Installing from pip::
pip install pynetics
Installing from source::
pip install git+https://github.com/blazaid/pynetics
Requirements
============
No requirements for now.
*************
Documentation
*************
WIP
*******
Authors
*******
`pynetics` was written by `Blazaid `_.
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.
PKHH&pynetics-0.4.0.dist-info/metadata.json{"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Artificial Intelligence"], "extensions": {"python.details": {"contacts": [{"email": "alberto.da@gmail.com", "name": "blazaid", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/blazaid/pynetics/"}}}, "generator": "bdist_wheel (0.26.0)", "license": "GNU General Public License v3", "metadata_version": "2.0", "name": "pynetics", "platform": "any", "summary": "An evolutionary computation library for Python", "test_requires": [{"requires": []}], "version": "0.4.0"}PKHHE/ &pynetics-0.4.0.dist-info/top_level.txtpynetics
PKHH}\\pynetics-0.4.0.dist-info/WHEELWheel-Version: 1.0
Generator: bdist_wheel (0.26.0)
Root-Is-Purelib: true
Tag: py3-none-any
PKHHM:[ !pynetics-0.4.0.dist-info/METADATAMetadata-Version: 2.0
Name: pynetics
Version: 0.4.0
Summary: An evolutionary computation library for Python
Home-page: http://github.com/blazaid/pynetics/
Author: blazaid
Author-email: alberto.da@gmail.com
License: GNU General Public License v3
Platform: any
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.5
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
==========
Pyvolution
==========
***********************************************
An evolutionary computation library for Python.
***********************************************
Pyvolution is a library for experimenting with evolutionary computation.
************
Installation
************
Installing from pip::
pip install pynetics
Installing from source::
pip install git+https://github.com/blazaid/pynetics
Requirements
============
No requirements for now.
*************
Documentation
*************
WIP
*******
Authors
*******
`pynetics` was written by `Blazaid `_.
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.
PKHH障pynetics-0.4.0.dist-info/RECORDpynetics/__init__.py,sha256=RW3g21vbD4CsH5zW-nCx96l4m-F7pICRhPtMEmRLUws,16871
pynetics/algorithms.py,sha256=BaN9PplyYIrPn3Y-eNA3a6Ptg8zexry10i2MY1s3Mgw,11935
pynetics/catastrophe.py,sha256=fR7izqV3gcn45Altvu5ukP50fKaK7lDMu9Lm0TkWgxQ,1973
pynetics/exceptions.py,sha256=nT_-6AJuzcKjSUSGpRE20mUzJzkv9JpHm15UexFOxRw,1982
pynetics/ga_bin.py,sha256=sD5fcM0Pkw7MkxoTzeaPXFImIVGMTntWH5Uh_DsYcfI,5729
pynetics/ga_int.py,sha256=pLD0x2pxUxwPXWY45woSfnj8IsA0_9aie29uZkexmdE,3286
pynetics/ga_list.py,sha256=5crwfmmC8Mi3wo2pQCGvZu2PewZ-N5XBr4AF-UkFe-4,12753
pynetics/ga_real.py,sha256=DkSmfNheTXgvSQc4QQMC-WvQL1lvG3s5vAtiLyjfk8Y,5096
pynetics/replacements.py,sha256=7eietbIMWc66MoA7DAfAWPYJxoC5aZpf-FEpNZCG9gc,1678
pynetics/selections.py,sha256=kyLir9hSrK3IPvMvbsrLatDPB-xYPhgNBqG6KEoBqhg,3568
pynetics/stop.py,sha256=9mhfIOJhUpWjMqDIEYCMxyB9ATgxwE5Qy7m-ssSu5fs,1976
pynetics/utils.py,sha256=qBnJD5udR-DxGtjx1A0Unndtj65aAWmHBmJLPtht0AM,763
pynetics/gggp/__init__.py,sha256=N4a-abt69wny6iqgcsLkBhWK1gaxIS-6oy2LJ8Sqr0o,23
pynetics/gggp/grammar.py,sha256=IhrjARipaEDjY5m1FV5A9cBE54boDnbRB-f9-ENIKUc,20732
pynetics/gggp/main.py,sha256=y6VYK-86-fKpUxRc86tIcQ1jHfpmV8mnzX9BlZ-ITaI,2893
pynetics/gggp/test_gggp_grammar.py,sha256=0jFSu4j4dmL2L09GJAnFE2nfIHHDy1qfXmYqOfdNfVg,20950
pynetics-0.4.0.dist-info/DESCRIPTION.rst,sha256=OiDtNPQyNiI4k30FST5cNxJ_1abuztcUyNKV_hUQpJc,1345
pynetics-0.4.0.dist-info/METADATA,sha256=RDZ6mF3MsNa6iCQLdjnE00G2LcynwLWvic5PNFFIMdA,2057
pynetics-0.4.0.dist-info/RECORD,,
pynetics-0.4.0.dist-info/WHEEL,sha256=zX7PHtH_7K-lEzyK75et0UBa3Bj8egCBMXe1M4gc6SU,92
pynetics-0.4.0.dist-info/metadata.json,sha256=GBM457KaRGH1g5o6jEEy4cngOQB_UG2n8ZUsSs5ynOU,903
pynetics-0.4.0.dist-info/top_level.txt,sha256=ScUGo5OAPTWWt0mnvqwVNWK7GXhKyJ670Hvbn92gS6A,9
PKHHGpynetics/ga_real.pyPK^lHSMpynetics/ga_int.pyPK^lHVV
!pynetics/selections.pyPK^lH*s8C/pynetics/replacements.pyPKHɁ116pynetics/ga_list.pyPK#H .. hpynetics/algorithms.pyPK,YlH|?ܖpynetics/utils.pyPK,YlHpynetics/stop.pyPKHHGuw&AApynetics/__init__.pyPK]mHYaapynetics/ga_bin.pyPK^lH\ݵpynetics/catastrophe.pyPK,YlHdpynetics/exceptions.pyPK,YlHC'PPr
pynetics/gggp/grammar.pyPK,YlHKQQ"[pynetics/gggp/test_gggp_grammar.pyPK,YlH˶4MMpynetics/gggp/main.pyPK,YlHQmw:pynetics/gggp/__init__.pyPKHHC"AA(pynetics-0.4.0.dist-info/DESCRIPTION.rstPKHH&pynetics-0.4.0.dist-info/metadata.jsonPKHHE/ &pynetics-0.4.0.dist-info/top_level.txtPKHH}\\'pynetics-0.4.0.dist-info/WHEELPKHHM:[ !pynetics-0.4.0.dist-info/METADATAPKHH障pynetics-0.4.0.dist-info/RECORDPK0