Skip to content

Commit

Permalink
deco
Browse files Browse the repository at this point in the history
  • Loading branch information
Freakwill committed Nov 28, 2023
1 parent 40f7c89 commit df50434
Show file tree
Hide file tree
Showing 26 changed files with 580 additions and 202 deletions.
Binary file added examples/QGA.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions examples/comparison-proba.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3

import copy

from pyrimidine import *
from pyrimidine.benchmarks.optimization import *

# generate a knapsack problem randomly
n_bags = 50
evaluate = Knapsack.random(n=n_bags)


class YourIndividual(BinaryChromosome // n_bags):

def _fitness(self):
return evaluate(self.decode())


class YourPopulation(HOFPopulation):
element_class = YourIndividual
default_size = 20


from pyrimidine.deco import add_memory

@add_memory({'measure_result': None, 'fitness': None})
class MyIndividual(QuantumChromosome // n_bags):

def _fitness(self):
return evaluate(self.decode())

def backup(self, check=False):
f = super().fitness
if not check or (self.memory['fitness'] is None or f > self.memory['fitness']):
self._memory = {
'measure_result': self.measure_result,
'fitness': f
}


class MyPopulation(HOFPopulation):

element_class = MyIndividual
default_size = 20

def init(self):
for i in self:
i.backup()
super().init()

def backup(self, check=True):
for i in self:
i.backup(check=check)

def transition(self, *args, **kwargs):
"""
Update the `hall_of_fame` after each step of evolution
"""
super().transition(*args, **kwargs)
self.backup()


stat={'Mean Fitness': 'mean_fitness', 'Best Fitness': 'best_fitness'}
mypop = MyPopulation.random()

yourpop = YourPopulation([YourIndividual(i.decode()) for i in mypop])
mydata = mypop.evolve(n_iter=100, stat=stat, history=True)
yourdata = yourpop.evolve(n_iter=100, stat=stat, history=True)

import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
yourdata[['Mean Fitness', 'Best Fitness']].plot(ax=ax)
mydata[['Mean Fitness', 'Best Fitness']].plot(ax=ax)
ax.legend(('Mean Fitness', 'Best Fitness', 'Mean Fitness(Quantum)', 'Best Fitness(Quantum)'))
ax.set_xlabel('Generations')
ax.set_ylabel('Fitness')
ax.set_title(f'Demo of (Quantum)GA: {n_bags}-Knapsack Problem')
plt.show()
4 changes: 2 additions & 2 deletions examples/example1.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def _fitness(self):
return _evaluate(self.decode())


MyPopulation = HOFPopulation[MyIndividual] // 12
MyPopulation = HOFPopulation[MyIndividual] // 16

pop = MyPopulation.random()
stat = {'Mean Fitness':'mean_fitness', 'Best Fitness':'best_fitness'}
Expand All @@ -47,5 +47,5 @@ def _fitness(self):
data[['Mean Fitness', 'Best Fitness']].plot(ax=ax)
ax.set_xlabel('Generations')
ax.set_ylabel('Fitness')
ax.set_title('Demo of GA')
ax.set_title('Demo for GA')
plt.show()
9 changes: 7 additions & 2 deletions examples/example4.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,28 @@ class _Chromosome(BinaryChromosome):
def decode(self):
return c(self)

from pyrimidine.deco import fitness_cache

@fitness_cache
class ExampleIndividual(PolyIndividual):
"""
You should implement the methods, cross, mute
"""

element_class = _Chromosome
default_size = 10

def _fitness(self):
x = self.decode()
return evaluate(x)


@fitness_cache
class MyIndividual(ExampleIndividual, SimulatedAnnealing):

def get_neighbour(self):
cpy = self.clone()
r = randint(0, self.n_chromosomes-1)
cpy.chromosomes[r].mutate()
cpy[r].mutate()
return cpy


Expand All @@ -57,6 +61,7 @@ def get_neighbour(self):
LocalSearchPopulation.element_class = MyIndividual

lga = LocalSearchPopulation.random(n_individuals=20, n_chromosomes=10, size=10)

lga.mate_prob = 0.9
d= lga.evolve(n_iter=10, stat=stat, history=True)
d[['Mean Fitness', 'Best Fitness']].plot(ax=ax, style='.-')
Expand Down
7 changes: 5 additions & 2 deletions examples/example5.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
evaluate = Function1DApproximation(function=lambda x:10*np.arctan(x), lb=-2, ub=2, basis=_my_basis)
n_basis = len(evaluate.basis)

from pyrimidine.deco import fitness_cache

@fitness_cache
class MyIndividual(makeIndividual(FloatChromosome, n_chromosomes=1, size=n_basis)):
def _fitness(self):
return evaluate(self.chromosome)
Expand All @@ -19,10 +22,10 @@ def _fitness(self):
class MyPopulation(HOFPopulation):
element_class = MyIndividual

pop = MyPopulation.random(n_individuals=200)
pop = MyPopulation.random(n_individuals=100)

stat = {'Best Fitness': 'best_fitness', 'Mean Fitness': 'mean_fitness'}
data = pop.evolve(n_iter=500, stat=stat, history=True)
data = pop.evolve(n_iter=200, stat=stat, history=True)


import matplotlib.pyplot as plt
Expand Down
2 changes: 1 addition & 1 deletion pyrimidine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .ba import *


__version__ = "1.4.2"
__version__ = "1.5.1"

__template__ = """
from pyrimidine import MonoBinaryIndividual
Expand Down
60 changes: 40 additions & 20 deletions pyrimidine/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MyPopulation(SGAPopulation):
from .meta import *
from .mixin import *

from .deco import clear_cache
from .deco import side_effect


class BaseGene:
Expand Down Expand Up @@ -133,11 +133,21 @@ def decode(self):

@classmethod
def encode(cls, x):
# encode x to a chromosome
raise NotImplementedError

def equal(self, other):
return np.array_equal(self, other)

def clone(self, *args, **kwargs):
raise NotImplementedError

def replicate(self):
# Replication operation of a chromosome
ind = self.clone()
ind.mutate()
return ind


class BaseIndividual(FitnessMixin, metaclass=MetaContainer):
"""Base class of individuals
Expand All @@ -149,7 +159,8 @@ class BaseIndividual(FitnessMixin, metaclass=MetaContainer):

element_class = BaseChromosome
default_size = 2
alias = {"chromosomes": "elements"}
alias = {"chromosomes": "elements",
"n_chromosomes": "n_elements"}

def __repr__(self):
# seperate the chromosomes with $
Expand Down Expand Up @@ -205,19 +216,23 @@ def _fitness(self):
else:
raise NotImplementedError

def cross(self, other, k=None):
def clone(self, type_=None):
if type_ is None:
type_ = self.__class__
return type_([c.clone(type_=type_.element_class) for c in self])

def cross(self, other):
# Cross operation of two individual
return self.__class__([chromosome.cross(other_c) for chromosome, other_c in zip(self.chromosomes, other.chromosomes)])

@clear_cache
@side_effect
def mutate(self, copy=False):
# Mutating operation of an individual
self.clear_cache()
for chromosome in self.chromosomes:
chromosome.mutate()
return self

def replicate(self, k=2):
def replicate(self):
# Replication operation of an individual
ind = self.clone()
ind.mutate()
Expand Down Expand Up @@ -256,7 +271,6 @@ def __mul__(self, n):
TYPE: BasePopulation
"""
assert isinstance(n, np.int_) and n>0, 'n must be a positive integer'
from .population import StandardPopulation
C = StandardPopulation[self.__class__]
return C([self.clone() for _ in range(n)])

Expand All @@ -270,7 +284,7 @@ def __rmul__(self, other):
return self.__class__([other * this for this in self.chromosomes])


class BasePopulation(PopulationMixin, metaclass=MetaHighContainer):
class BasePopulation(PopulationMixin, metaclass=MetaContainer):
"""The base class of population in GA
Represents a state of a stachostic process (Markov process)
Expand Down Expand Up @@ -356,7 +370,6 @@ def select(self, n_sel=None, tournsize=None):
else:
raise Exception('No winners in the selection!')

@clear_cache
def merge(self, other, n_sel=None):
"""Merge two populations.
Expand All @@ -366,16 +379,16 @@ def merge(self, other, n_sel=None):
"""

if isinstance(other, BasePopulation):
self.individuals += other.individuals
self.extend(other.individuals)
elif isinstance(other, typing.Iterable):
self.individuals.extend(other)
self.extend(other)
else:
raise TypeError("`other` should be a population or a list/tuple of individuals")

if n_sel:
self.select(n_sel)

@clear_cache
@side_effect
def mutate(self, mutate_prob=None):
"""Mutate the whole population.
Expand All @@ -389,7 +402,6 @@ def mutate(self, mutate_prob=None):
if random() < (mutate_prob or self.mutate_prob):
individual.mutate()

@clear_cache
def mate(self, mate_prob=None):
"""Mate the whole population.
Expand All @@ -402,9 +414,10 @@ def mate(self, mate_prob=None):
mate_prob = mate_prob or self.mate_prob
offspring = [individual.cross(other) for individual, other in zip(self.individuals[::2], self.individuals[1::2])
if random() < mate_prob]
self.individuals.extend(offspring)
self.extend(offspring)
self.offspring = self.__class__(offspring)

@side_effect
def local_search(self, *args, **kwargs):
"""Call local searching method
Expand Down Expand Up @@ -454,10 +467,14 @@ def rank(self, tied=False):
for k, i in enumerate(sorted_list):
i.ranking = k / self.n_individuals

@clear_cache
def cross(self, other):
# Cross two populations as two individuals
k = randint(1, self.n_individuals-2)
return self.__class__(self[k:] + other[:k])

def migrate(self, other):
# migrate between two populations
k = randint(1, self.n_individuals-2)
self.individuals = self[k:] + other[:k]
other.individuals = other[k:] + self[:k]

Expand All @@ -470,7 +487,7 @@ class BaseMultiPopulation(PopulationMixin, metaclass=MetaHighContainer):
Attributes:
default_size (int): the number of populations
element_class (TYPE): type of the populations
element_class (TYPE): the type of the populations
elements (TYPE): populations as the elements
fitness (TYPE): best fitness
"""
Expand Down Expand Up @@ -499,8 +516,8 @@ def random(cls, n_populations=None, *args, **kwargs):
def migrate(self, migrate_prob=None):
for population, other in zip(self[:-1], self[1:]):
if random() < (migrate_prob or self.migrate_prob):
other.individuals.append(population.best_individual.clone())
population.individuals.append(other.best_individual.clone())
other.append(population.get_best_individual(copy=True))
population.append(other.get_best_individual(copy=True))

def transition(self, *args, **kwargs):
super().transition(*args, **kwargs)
Expand All @@ -509,10 +526,13 @@ def transition(self, *args, **kwargs):
def best_fitness(self):
return max(map(attrgetter('best_fitness'), self))

def get_best_individual(self):
def get_best_individual(self, copy=True):
bests = map(methodcaller('get_best_individual'), self)
k = np.argmax([b.fitness for b in bests])
return bests[k]
if copy:
return bests[k].clone()
else:
return bests[k]

@property
def individuals(self):
Expand Down
2 changes: 1 addition & 1 deletion pyrimidine/benchmarks/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class KMeans:
ERM:
min J(c,mu) = sum_c sum_{x:c} ||x-mu_c||
"""

def __init__(self, X, n_components=2):
self.X = X
self.n_components = n_components
Expand All @@ -21,7 +22,6 @@ def random(N, p=2):
X = np.vstack((X1, X2))
return KMeans(X, n_components=2)


def __call__(self, x):
# xi = k iff Xi in k-class
cs = set(x)
Expand Down
Loading

0 comments on commit df50434

Please sign in to comment.