From df5043455e9a5c1bab733000214f7e19f4210cda Mon Sep 17 00:00:00 2001 From: William Song <30965609+Freakwill@users.noreply.github.com> Date: Tue, 28 Nov 2023 18:33:34 +0800 Subject: [PATCH] deco --- examples/QGA.png | Bin 0 -> 40993 bytes examples/comparison-proba.py | 79 +++++++ examples/example1.py | 4 +- examples/example4.py | 9 +- examples/example5.py | 7 +- pyrimidine/__init__.py | 2 +- pyrimidine/base.py | 60 ++++-- pyrimidine/benchmarks/cluster.py | 2 +- pyrimidine/benchmarks/matrix.py | 37 +++- pyrimidine/benchmarks/optimization.py | 14 +- pyrimidine/chromosome.py | 37 ++-- pyrimidine/deco.py | 198 ++++++++++++++++++ pyrimidine/gene.py | 10 +- pyrimidine/gsa.py | 11 +- pyrimidine/individual.py | 34 +-- .../local_search/simulated_annealing.py | 2 +- pyrimidine/meta.py | 21 +- pyrimidine/mixin.py | 91 +++----- pyrimidine/multipopulation.py | 8 +- pyrimidine/population.py | 31 ++- pyrimidine/saga.py | 2 +- tests/test_de.py | 58 ++--- tests/test_deco.py | 28 +++ tests/test_pso.py | 15 +- tests/test_utils.py | 2 +- travis.yml | 20 ++ 26 files changed, 580 insertions(+), 202 deletions(-) create mode 100644 examples/QGA.png create mode 100755 examples/comparison-proba.py create mode 100755 pyrimidine/deco.py create mode 100644 tests/test_deco.py create mode 100644 travis.yml diff --git a/examples/QGA.png b/examples/QGA.png new file mode 100644 index 0000000000000000000000000000000000000000..a10d6949eeea4f46edeffa6f700a2098cd2dc0b6 GIT binary patch literal 40993 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 C5(4x9 literal 0 HcmV?d00001 diff --git a/examples/comparison-proba.py b/examples/comparison-proba.py new file mode 100755 index 0000000..e118c42 --- /dev/null +++ b/examples/comparison-proba.py @@ -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() diff --git a/examples/example1.py b/examples/example1.py index 4c2ec66..366d63b 100755 --- a/examples/example1.py +++ b/examples/example1.py @@ -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'} @@ -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() diff --git a/examples/example4.py b/examples/example4.py index 3d43140..e5aced0 100755 --- a/examples/example4.py +++ b/examples/example4.py @@ -19,10 +19,14 @@ 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 @@ -30,13 +34,13 @@ 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 @@ -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='.-') diff --git a/examples/example5.py b/examples/example5.py index a5eb380..d42371f 100755 --- a/examples/example5.py +++ b/examples/example5.py @@ -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) @@ -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 diff --git a/pyrimidine/__init__.py b/pyrimidine/__init__.py index 6ef402c..8d99087 100755 --- a/pyrimidine/__init__.py +++ b/pyrimidine/__init__.py @@ -16,7 +16,7 @@ from .ba import * -__version__ = "1.4.2" +__version__ = "1.5.1" __template__ = """ from pyrimidine import MonoBinaryIndividual diff --git a/pyrimidine/base.py b/pyrimidine/base.py index b9dfd53..0202a47 100755 --- a/pyrimidine/base.py +++ b/pyrimidine/base.py @@ -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: @@ -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 @@ -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 $ @@ -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() @@ -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)]) @@ -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) @@ -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. @@ -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. @@ -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. @@ -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 @@ -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] @@ -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 """ @@ -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) @@ -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): diff --git a/pyrimidine/benchmarks/cluster.py b/pyrimidine/benchmarks/cluster.py index 1006cfb..d904b6a 100755 --- a/pyrimidine/benchmarks/cluster.py +++ b/pyrimidine/benchmarks/cluster.py @@ -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 @@ -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) diff --git a/pyrimidine/benchmarks/matrix.py b/pyrimidine/benchmarks/matrix.py index bd9fbdb..1178115 100755 --- a/pyrimidine/benchmarks/matrix.py +++ b/pyrimidine/benchmarks/matrix.py @@ -7,10 +7,9 @@ class NMF: # M ~ A diag(C) B' + def __init__(self, M): self.M = M - self.norm = LA.norm(M) - @staticmethod def random(N=500, p=100): @@ -20,14 +19,44 @@ def random(N=500, p=100): M[k] /= s[k] return NMF(M=M) + def __call__(self, A, B, C=None): + """A: N * K + C: K + B: K * p + """ + + c = A.shape[1] + if C is not None: + for i in range(c): + A[:,i] *= C[i] + return - LA.norm(self.M - np.dot(A, B)) + + +class SparseMF: + # M ~ A B' + + def __init__(self, M, threshold=0.01): + self.M = M + self.threshold = threshold + + @staticmethod + def random(N=500, p=100): + M = np.random.rand(N, p) * 10 + s = M.sum(axis=1) + for k in range(N): + M[k] /= s[k] + return NMF(M=M) def __call__(self, A, B, C=None): """A: N * K C: K B: K * p """ + c = A.shape[1] if C is not None: for i in range(c): - A[:,i] = A[:,i] * C[i] - return - LA.norm(self.M - np.dot(A, B)) / self.norm + A[:,i] *= C[i] + T = B > threshold + B *= T + return - LA.norm(self.M - np.dot(A, B)) - np.sum(T) diff --git a/pyrimidine/benchmarks/optimization.py b/pyrimidine/benchmarks/optimization.py index 6fae410..2428c51 100755 --- a/pyrimidine/benchmarks/optimization.py +++ b/pyrimidine/benchmarks/optimization.py @@ -4,7 +4,6 @@ from .benchmarks import Problem - class Knapsack(BaseProblem): """Knapsack Problem @@ -67,7 +66,7 @@ def __call__(self, x): return - 1/(1 + np.exp(-v)) * M -class MultiKnapsack(Problem): +class MultiKnapsack(BaseProblem): """Multi Choice Knapsack Problem max sum_ij cij xij @@ -126,7 +125,7 @@ def __call__(self, x): return - 1/(1 + np.exp(-v)) * M -class MLE: +class MLE(BaseProblem): # max likelihood estimate def __init__(self, pdf, x): self.pdf = logpdf @@ -142,7 +141,7 @@ def __call__(self, t): return np.sum([self.logpdf(xi, *t) for xi in self.x]) -class MixMLE: +class MixMLE(MLE): # mix version of max likelihood estimate # x|k ~ pk def __init__(self, pdfs, x): @@ -167,7 +166,7 @@ def __call__(self, t, a): from scipy.spatial.distance import pdist, squareform -class ShortestPath: +class ShortestPath(BaseProblem): def __init__(self, points): """TSP @@ -204,7 +203,7 @@ def _heart(t, a=1): heart_path = CurvePath(x, y) -class MinSpanningTree: +class MinSpanningTree(BaseProblem): def __init__(self, nodes, edges=[]): self.nodes = nodes self.edges = edges @@ -224,7 +223,8 @@ def prufer_decode(self, x): edges.append(tuple(Q)) return edges -class FacilityLayout(object): + +class FacilityLayout(BaseProblem): ''' F: F D: D diff --git a/pyrimidine/chromosome.py b/pyrimidine/chromosome.py index d5f2405..6f8e5f5 100755 --- a/pyrimidine/chromosome.py +++ b/pyrimidine/chromosome.py @@ -7,7 +7,6 @@ from .base import BaseChromosome, BaseGene from .gene import * -from .deco import clear_cache def _asarray(out): @@ -29,8 +28,7 @@ def __new__(cls, array=None, element_class=None): if array is None: array = [] - o = np.asarray(array, dtype=element_class).view(cls) - return o + return np.asarray(array, dtype=element_class).view(cls) def __array_finalize__(self, obj): if obj is None: @@ -87,12 +85,12 @@ def cross(self, other): k = randint(1, len(self)-1) return self.__class__(np.concatenate((self[:k], other[k:]), axis=0)) - def clone(self, *args, **kwargs): - o = self.copy() - o.set_cache(**self._cache) - return o + def clone(self, type_=None): + if type_ is None: + return self.__class__(np.copy(self)) + else: + return type_(np.copy(self)) - @clear_cache def mutate(self, indep_prob=0.1): for i in range(len(self)): if random() < indep_prob: @@ -106,7 +104,6 @@ class VectorChromosome(NumpyArrayChromosome): class MatrixChromosome(NumpyArrayChromosome): - @clear_cache def mutate(self, indep_prob=0.1): r, c = self.shape for i in range(r): @@ -126,24 +123,26 @@ class NaturalChromosome(VectorChromosome): element_class = NaturalGene - @clear_cache def mutate(self, indep_prob=0.1): for i in range(len(self)): if random()< indep_prob: self[i] = NaturalGene.random() - def __str__(self): - return "".join(map(str, self)) - def dual(self): return self.__class__(self.element_class.ub - self) +class DigitChromosome(NaturalChromosome): + + element_class = DigitGene + + def __str__(self): + return "".join(map(str, self)) + class BinaryChromosome(NaturalChromosome): element_class = BinaryGene - @clear_cache def mutate(self, indep_prob=0.5): for i in range(len(self)): if random() < indep_prob: @@ -171,7 +170,6 @@ def move_toward(self, other): r = choice(rotations(self, other)) rotate(self, r) - @clear_cache def mutate(self): i, j = randint2(0, self.default_size-1) self[[i,j]] = self[[j,i]] @@ -194,7 +192,9 @@ class FloatChromosome(NumpyArrayChromosome): element_class = FloatGene sigma = 0.05 - @clear_cache + def __str__(self): + return "|".join(format(c, '.4') for c in self) + def mutate(self, indep_prob=0.1, mu=0, sigma=None): sigma = sigma or self.sigma for i in range(len(self)): @@ -325,7 +325,7 @@ def normalize(self): class QuantumChromosome(CircleChromosome): - + measure_result = None def decode(self): @@ -372,10 +372,9 @@ def cross(self, other): k = randint(1, len(self)-1) return self[:k] + other[k:] - def clone(self, *args, **kwargs): + def clone(self, type_=None): return copy.copy(self) - @clear_cache def mutate(self, indep_prob=0.1): a = self.random() for k in range(len(self)): diff --git a/pyrimidine/deco.py b/pyrimidine/deco.py new file mode 100755 index 0000000..1ace938 --- /dev/null +++ b/pyrimidine/deco.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 + +""" +Decorators +""" + +from types import MethodType + +import copy + + +def clear_cache(func): + def mthd(obj, *args, **kwargs): + result = func(obj, *args, **kwargs) + obj.clear_cache() + return result + return mthd + +def side_effect(func): + """Decorator for methods with side effect + + Apply the decorator to methods with side effects. + If all the methods called by a particular method have the decorator applied, + it is not advisable to include the decorator in that method. + + Args: + func (TYPE): a method + + Returns: + Decorated method + """ + + def mthd(obj, *args, **kwargs): + result = func(obj, *args, **kwargs) + if hasattr(obj, '_cache'): + obj.clear_cache() + return result + return mthd + + +def clear_fitness(func): + def mthd(obj, *args, **kwargs): + result = func(obj, *args, **kwargs) + obj.clear_cache('fitness') + return result + return mthd + + +class add_memory: + + def __init__(self, memory={}): + self._memory = memory + + def __call__(self, cls): + + cls._memory = self._memory + + def memory(obj): + return obj._memory + + cls.memory = property(memory) + + def fitness(obj): + if obj.memory['fitness'] is not None: + return obj.memory['fitness'] + else: + return obj._fitness() + + cls.fitness = property(fitness) + + cls_clone = cls.clone + def _clone(obj, *args, **kwargs): + cpy = cls_clone(obj, *args, **kwargs) + cpy._memory = obj._memory + return cpy + cls.clone = _clone + + cls_new = cls.__new__ + def _new(cls, *args, **kwargs): + obj = cls_new(cls, *args, **kwargs) + obj._memory = copy.copy(cls._memory) + return obj + + cls.__new__ = _new + + return cls + + +usual_side_effect = ['mutate', 'extend', 'pop', 'remove'] + + +class add_cache: + + """Handle with cache for class + + Attributes: + attrs (tuple[str]): a tuple of attributes + methods (tuple[str]): a tuple of method names + """ + + def __init__(self, attrs, methods=()): + self.methods = methods + self.attrs = attrs + + def __call__(self, cls): + cls._cache = {a: None for a in self.attrs} + + @property + def cache(obj): + return obj._cache + + cls.cache = property(cache) + + def _clear_cache(obj, k=None): + if k is None: + obj._cache = {k: None for k in obj._cache.keys()} + elif k in obj._cache: + obj._cache[k] = None + + def _cleared(obj, k=None): + if k is None: + return all(v == None for v in obj._cache.values()) + elif k in obj._cache: + return obj._cache[k] == None + + def _set_cache(obj, **d): + obj._cache.update(d) + + cls_clone = cls.clone + def _clone(obj, cache=True, *args, **kwargs): + cpy = cls_clone(obj, *args, **kwargs) + if cache: + cpy.set_cache(**obj._cache) + return cpy + + cls.cleared = _cleared + cls.clear_cache = _clear_cache + cls.set_cache = _set_cache + cls.clone = _clone + + for a in self.attrs: + def f(obj): + if obj._cache[a] is None: + f = getattr(obj, '_'+a)() + obj._cache[a] = f + return f + else: + return obj._cache[a] + setattr(cls, a, property(f)) + + def _after_setter(obj): + obj.clear_cache() + + cls.after_setter = _after_setter + + for name in self.methods: + if hasattr(obj, name): + setattr(cls, name, clear_cache(getattr(cls, name))) + + cls_new = cls.__new__ + def _new(cls, *args, **kwargs): + obj = cls_new(cls, *args, **kwargs) + obj._cache = copy.copy(cls._cache) + return obj + + cls.__new__ = _new + + return cls + + +fitness_cache = add_cache(('fitness',)) + + +class Regester: + # regerster operators, used in the future version + + def __init__(name, key=None): + self.name = name + self.key = key + + def __call__(cls): + + def _regester_operator(self, name, key=None): + if hasattr(self, name): + raise AttributeError(f'"{name}" is an attribute of "{self.__name__}", and would not be regestered.') + self._operators.append(key) + setattr(self, name, MethodType(key, self)) + + def _element_regester(self, name, e): + if hasattr(self, e): + raise AttributeError(f'"{e}" is an attribute of "{self.__name__}", would not be regestered.') + self._elements.append(e) + setattr(self, name, e) + + cls.regester_operator = _regester_operator + cls.regester_element = _regester_element + + return cls diff --git a/pyrimidine/gene.py b/pyrimidine/gene.py index 593b779..4287ebb 100755 --- a/pyrimidine/gene.py +++ b/pyrimidine/gene.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 +""" +Gene classes +""" + import numpy as np from . import BaseGene @@ -11,6 +15,9 @@ class NaturalGene(np.int_, BaseGene): def random(cls, *args, **kwargs): return np.random.randint(cls.ub, dtype=cls, *args, **kwargs) +class DigitGene(NaturalGene): + pass + class IntegerGene(np.int_, BaseGene): lb, ub = -10, 10 @@ -49,7 +56,8 @@ def period(self): class CircleGene(PeriodicGene): - lb, ub = -np.pi, np.pi + lb, ub = 0, 2*np.pi + period = 2*np.pi class SemiCircleGene(CircleGene): diff --git a/pyrimidine/gsa.py b/pyrimidine/gsa.py index dd478d2..e11da16 100644 --- a/pyrimidine/gsa.py +++ b/pyrimidine/gsa.py @@ -24,7 +24,6 @@ class Particle(PolyIndividual): default_size = 2 accelerate = 0 - @property def position(self): return self.chromosomes[0] @@ -32,7 +31,7 @@ def position(self): @position.setter def position(self, x): self.chromosomes[0] = x - self.__fitness = None + self._cache['fitness'] = None @property def velocity(self): @@ -51,14 +50,14 @@ def move(self): D = cpy.fitness - self.fitness if flag: self.chromosomes = cpy.chromosomes - self.__fitness = cpy.fitness + self._cache['fitness'] = cpy.fitness class GravitySearch(PopulationMixin): """Standard GSA Extends: - BaseFitnessMixin + PopulationMixin """ element_class = Particle @@ -74,7 +73,6 @@ def compute_mass(self): m = (fitnesses - worst_fitness + epsilon) / (best_fitness - worst_fitness + epsilon) return m / m.sum() - def compute_accelerate(self): # compute force D = np.array([[pj.position - pi.position for pi in self] for pj in self]) @@ -92,9 +90,8 @@ def compute_accelerate(self): # set accelerate for i, particle in enumerate(self): - particle.accelerate = A[i,:] + particle.accelerate = A[i, :] - def transition(self, k): """ Transitation of the states of particles diff --git a/pyrimidine/individual.py b/pyrimidine/individual.py index ca8c7ec..a1a7a7e 100755 --- a/pyrimidine/individual.py +++ b/pyrimidine/individual.py @@ -85,17 +85,14 @@ def gender(self): raise NotImplementedError +from .deco import add_memory, add_cache + +@add_memory(memory={"fitness": None}) class MemoryIndividual(BaseIndividual): # Individual with memory, used in PSO - _memory = {"fitness": None} - - @property - def memory(self): - return self._memory - def init(self, fitness=True, type_=None): - self._memory = {k: None for k in self.__class__.memory.keys()} + self._memory = {k: None for k in self.__class__._memory.keys()} self.backup(check=False) def backup(self, check=False): @@ -105,7 +102,7 @@ def backup(self, check=False): check (bool, optional): check whether the fitness increases. """ - f = self._fitness() + f = super().fitness if not check or (self.memory['fitness'] is None or f > self.memory['fitness']): def _map(k): if k == 'fitness': @@ -118,31 +115,22 @@ def _map(k): return getattr(self, k) self._memory = {k: _map(k) for k in self.memory.keys()} - @property - def fitness(self): - if 'fitness' in self.memory and self.memory['fitness'] is not None: - return self.memory['fitness'] - else: - return super().fitness - - def clone(self, *args, **kwargs): - cpy = super().clone(*args, **kwargs) - cpy._memory = self.memory - return cpy - +@add_cache(('fitness',)) class PhantomIndividual(BaseIndividual): # Another implementation of the individual class with memory + _cache = {'fitness': None} + phantom = None - def init(self, fitness=True, type_=None): - self.phantom = self.clone(fitness=fitness, type_=type_) + def init(self): + self.phantom = self.clone() def backup(self): if self.fitness < self.phantom.fitness: self.chromosomes = self.phantom.chromosomes - self.cache_fitness(self.phantom.fitness) + self.set_cache(fitness=self.phantom.fitness) # Following are functions to create individuals diff --git a/pyrimidine/local_search/simulated_annealing.py b/pyrimidine/local_search/simulated_annealing.py index 51be3ae..e5aebf4 100755 --- a/pyrimidine/local_search/simulated_annealing.py +++ b/pyrimidine/local_search/simulated_annealing.py @@ -32,7 +32,7 @@ class SimulatedAnnealing(PhantomIndividual): def init(self): # initialize phantom solution - self.phantom = self.clone(fitness=None) + self.phantom = self.clone() def transition(self, *args, **kwargs): T = self.initT diff --git a/pyrimidine/meta.py b/pyrimidine/meta.py index 8a6b977..db5fdd9 100755 --- a/pyrimidine/meta.py +++ b/pyrimidine/meta.py @@ -49,9 +49,7 @@ def __new__(cls, name, bases=(), attrs={}): attrs = inherit(attrs, 'params', bases) def _getattr(self, key): - if key in self.__dict__: - return self.__dict__[key] - elif key in self.params: + if key in self.params: return self.params[key] elif key in self.alias: return getattr(self, self.alias[key]) @@ -110,6 +108,11 @@ class cls(self, other): pass return cls + def __call__(self, *args, **kwargs): + obj = super().__call__(*args, **kwargs) + obj.params = copy.deepcopy(self.params) + return obj + class MetaContainer(ParamType): """Meta class of containers @@ -171,6 +174,7 @@ def _getitem(self, k): def _setitem(self, k, v): # print(DeprecationWarning('get item directly is not recommended now.')) self.__elements[k] =v + self.after_setter() def _iter(self): return iter(self.__elements) @@ -340,7 +344,7 @@ def __getitem__(self, class_): def __call__(self, *args, **kwargs): o = super().__call__(*args, **kwargs) - for e in o.__elements: # consider in future + for e in o: # consider in future if not isinstance(e, self.element_class): raise TypeError(f'"{e}" is not an instance of type "{self.element_class}"') return o @@ -362,7 +366,7 @@ def __floordiv__(self, n): def __call__(self, *args, **kwargs): o = super().__call__(*args, **kwargs) - for e, t in (o.__elements, self.element_class): # consider in future + for e, t in zip(o, self.element_class): # consider in future if not isinstance(e, t): raise TypeError(f'"{e}" is not an instance of type "{t}"') return o @@ -452,9 +456,8 @@ def __floordiv__(self, n): return self.set(default_size=n) def __call__(self, *args, **kwargs): - o = super().__call__(*args, **kwargs) - o.params = copy.deepcopy(self.params) + obj = super().__call__(*args, **kwargs) if hasattr(self, '_cache'): - o._cache = copy.copy(self._cache) - return o + obj._cache = copy.copy(self._cache) + return obj diff --git a/pyrimidine/mixin.py b/pyrimidine/mixin.py index 2d45445..14b43a3 100755 --- a/pyrimidine/mixin.py +++ b/pyrimidine/mixin.py @@ -26,27 +26,13 @@ from ezstat import Statistics from .errors import * -from .deco import clear_cache +from .deco import side_effect class IterativeMixin: # Mixin class for iterative algrithms params = {'n_iter': 100} - _cache = {} - - @property - def cache(self): - return self._cache - - def clear_cache(self, k=None): - if k is None: - self._cache = {k: None for k in self._cache.keys()} - elif k in self._cache: - self._cache[k] = None - - def set_cache(self, **d): - self._cache.update(d) @property def solution(self): @@ -167,6 +153,9 @@ def perf(self, n_repeats=10, *args, **kwargs): def clone(self, type_=None, *args, **kwargs): raise NotImplementedError + def copy(self): + raise NotImplementedError + def encode(self): raise NotImplementedError @@ -190,6 +179,10 @@ def load(filename='model.pkl'): else: raise FileNotFoundError(f'Could not find the file {filename}!') + def after_setter(self): + if hasattr(self, '_cache'): + self.clear_cache() + class FitnessMixin(IterativeMixin): """Iterative models drived by the fitness/objective function @@ -197,21 +190,9 @@ class FitnessMixin(IterativeMixin): The fitness should be stored until the the state of the model is changed. Extends: - BaseIterativeMixin + IterativeMixin """ - _cache = {'fitness': None} - - def cache_fitness(self, v): - self._cache['fitness'] = v - - @property - def fitness(self): - if self._cache['fitness'] is None: - f = self._fitness() - self.cache_fitness(f) - return self._cache['fitness'] - def get_fitness(self): raise NotImplementedError @@ -219,6 +200,10 @@ def _fitness(self): # the alias of the fitness return self.get_fitness() + @property + def fitness(self): + return self._fitness() + @classmethod def set_fitness(cls, f=None): if f is None: @@ -226,22 +211,8 @@ def set_fitness(cls, f=None): f = globals()['_fitness'] else: raise Exception('Function `_fitness` is not defined before setting fitness. You may forget to create the class in the context of environment.') - class C(cls): - def _fitness(self): - return f(self) - return C - - def clone(self, type_=None, fitness=True): - if type_ is None: - type_ = self.__class__ - if fitness is True: - fitness = self.fitness - cpy = type_(list(map(methodcaller('clone', type_=type_.element_class, fitness=True), self))) - if fitness is True: - cpy.cache_fitness(self.fitness) - else: - cpy.cache_fitness(fitness) - return cpy + cls._fitness = f + return cls def evolve(self, stat=None, attrs=('solution',), *args, **kwargs): """Get the history of solution and its fitness by default. @@ -251,9 +222,9 @@ def evolve(self, stat=None, attrs=('solution',), *args, **kwargs): stat = {'Fitness':'fitness'} return super().evolve(stat=stat, attrs=attrs, *args, **kwargs) - def after_setter(self): - # clean up the fitness after updating the chromosome - self.clear_cache() + @property + def solution(self): + return self class ContainerMixin(IterativeMixin): @@ -264,26 +235,25 @@ def init(self, *args, **kwargs): for element in self: element.init(*args, **kwargs) - @clear_cache def transition(self, *args, **kwargs): for element in self: element.transition(*args, **kwargs) - @clear_cache + @side_effect def remove(self, individual): self.elements.remove(individual) - @clear_cache + @side_effect def pop(self, k=-1): self.elements.pop(k) - @clear_cache + @side_effect def extend(self, inds): self.elements.extend(inds) - @clear_cache - def add_individuals(self, inds): - self.elements.extend(inds) + @side_effect + def append(self, ind): + self.elements.append(ind) class PopulationMixin(FitnessMixin, ContainerMixin): @@ -301,9 +271,6 @@ def evolve(self, stat=None, *args, **kwargs): 'STD Fitness': 'std_fitness', 'Population': 'n_elements'} return super().evolve(stat=stat, *args, **kwargs) - def after_setter(self): - self.clear_cache() - @classmethod def set_fitness(cls, *args, **kwargs): # set fitness for the element_class. @@ -426,3 +393,13 @@ def drop(self, n=1): n = int(n) ks = self.argsort() self.elements = [self[k] for k in ks[n:]] + + def clone(self, type_=None, fitness=True): + if type_ is None: + type_ = self.__class__ + cpy = type_(list(map(methodcaller('clone', type_=type_.element_class, fitness=True), self))) + if fitness is True: + cpy.cache_fitness(self.fitness) + else: + cpy.cache_fitness(fitness) + return cpy diff --git a/pyrimidine/multipopulation.py b/pyrimidine/multipopulation.py index 1abebe8..2850a38 100755 --- a/pyrimidine/multipopulation.py +++ b/pyrimidine/multipopulation.py @@ -59,16 +59,16 @@ def _target(male, female): for p in ps: p.join() - self.populations[0].add_individuals(children[::2]) - self.populations[1].add_individuals(children[1::2]) + self.populations[0].extend(children[::2]) + self.populations[1].extend(children[1::2]) def match(self, male, female): return True def transition(self, *args, **kwargs): elder = self.__class__([ - self.populations[0].clone_best_individuals(self.n_elders * self.populations[0].default_size), - self.populations[1].clone_best_individuals(self.n_elders * self.populations[1].default_size) + self.populations[0].get_best_individuals(self.n_elders * self.populations[0].default_size, copy=True), + self.populations[1].get_best_individuals(self.n_elders * self.populations[1].default_size, copy=True) ]) self.select() self.mate() diff --git a/pyrimidine/population.py b/pyrimidine/population.py index 4404b79..d72873a 100755 --- a/pyrimidine/population.py +++ b/pyrimidine/population.py @@ -16,7 +16,7 @@ from .chromosome import BinaryChromosome from .meta import MetaList -from .deco import clear_cache +from .deco import side_effect class StandardPopulation(BasePopulation): @@ -69,7 +69,7 @@ def transition(self, *args, **kwargs): super().transition(*args, **kwargs) self.update_hall_of_fame() - self.add_individuals(self.hall_of_fame) + self.extend(self.hall_of_fame) def update_hall_of_fame(self): for ind in self: @@ -165,7 +165,6 @@ def transition(self, *args, **kwargs): self.eliminate() self.merge(elder) - @clear_cache def eliminate(self): # remove some individuals randomly from the population for individual in self: @@ -180,7 +179,6 @@ def transition(self, *args, **kwargs): individual.age += 1 super().transition(*args, **kwargs) - @clear_cache def eliminate(self): # remove some old individuals for individual in self: @@ -192,6 +190,12 @@ class LocalSearchPopulation(StandardPopulation): """Population with `local_search` method """ + params = {'n_local_iter': 10} + + def init(self): + for i in self: + i.n_iter = self.n_local_iter + def transition(self, *args, **kwargs): """Transitation of the states of population @@ -205,7 +209,6 @@ class ModifiedPopulation(StandardPopulation): params = {'mutate_prob_ub':0.5, 'mutate_prob_lb':0.1} - @clear_cache def mutate(self): fm = self.best_fitness fa = self.mean_fitness @@ -219,11 +222,21 @@ def mutate(self): individual.mutate() -def makeBinaryPopulation(cls=None, n_populations=20, size=8, as_chromosome=True): - # make a binary population +def makeBinaryPopulation(n_individuals=20, size=8, as_chromosome=True, cls=None): + """Make a binary population + + Args: + n_individuals (int, optional): the number of the individuals in the population + size (int, optional): the size of an individual or a chromosome + as_chromosome (bool, optional): take chromosomes as the individuals of the population + cls (None, optional): the type of the population (HOFPopulation by default) + + Returns: + subclass of BasePopulation + """ cls = cls or HOFPopulation if as_chromosome: - return cls[BinaryChromosome // size] // n_populations + return cls[BinaryChromosome // size] // n_individuals else: - return cls[binaryIndividual(size)] // n_populations + return cls[binaryIndividual(size)] // n_individuals diff --git a/pyrimidine/saga.py b/pyrimidine/saga.py index 5f7554c..fd33f88 100755 --- a/pyrimidine/saga.py +++ b/pyrimidine/saga.py @@ -126,7 +126,7 @@ def mate(self): if random() < min(individual.cross_prob, other.cross_prob): if self.match(individual, other): children.append(individual.cross(other)) - self.add_individuals(children) + self.extend(children) @classmethod def match(cls, individual, other): diff --git a/tests/test_de.py b/tests/test_de.py index 1bf3a15..15cb847 100644 --- a/tests/test_de.py +++ b/tests/test_de.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import unittest from pyrimidine.individual import MonoIndividual from pyrimidine.population import HOFPopulation, BasePopulation @@ -9,41 +8,50 @@ from pyrimidine.benchmarks.special import rosenbrock +import pytest -class TestDE(unittest.TestCase): +@pytest.fixture(scope="class") +def pop(): + n = 20 + f = rosenbrock - def setUp(self): + class MyIndividual(MonoIndividual): + element_class = FloatChromosome // n - n = 20 - f = rosenbrock + def _fitness(self): + return -f(self.chromosome) - class MyIndividual(MonoIndividual): - element_class = FloatChromosome // n + class _Population1(DifferentialEvolution, BasePopulation): + element_class = MyIndividual + default_size = 10 - def _fitness(self): - return -f(self.chromosome) + class _Population2(HOFPopulation): + element_class = MyIndividual + default_size = 10 - class _Population1(DifferentialEvolution, BasePopulation): - element_class = MyIndividual - default_size = 10 + # _Population2 = HOFPopulation[MyIndividual] // 10 - class _Population2(HOFPopulation): - element_class = MyIndividual - default_size = 10 + return _Population1, _Population2 - # _Population2 = HOFPopulation[MyIndividual] // 10 - self.Population1 = _Population1 - self.population1 = Population1.random() - self.Population2 = _Population2 - self.population2 = Population2.random() +class TestDE: + + @classmethod + def setup_class(cls): + cls.Populations = cls.request.getfixturevalue("pop") + def test_clone(self): - self.population2 = self.population1.clone(type_=self.Population2) # population 2 with the same initial values to population 1 - assert isinstance(self.population2, self.Population2) + P1, P2 = cls.Populations + p2 = P1.random().clone(type_=P2) + assert isinstance(p2, P2) def test_evolve(self): + P1, P2 = cls.Populations + self.population1 = P1.random() + self.population2 = P2.random() stat={'Mean Fitness':'mean_fitness', 'Best Fitness':'best_fitness'} - data1 = self.population1.evolve(stat=stat, n_iter=10, history=True) - data2 = self.population2.evolve(stat=stat, n_iter=10, history=True) - assert True + data1 = self.population1.evolve(stat=stat, n_iter=5, history=True) + data2 = self.population2.evolve(stat=stat, n_iter=5, history=True) + assert ('Mean Fitness' in data1.columns and 'Best Fitness' in data1.columns and + 'Mean Fitness' in data2.columns and 'Best Fitness' in data2.columns) diff --git a/tests/test_deco.py b/tests/test_deco.py new file mode 100644 index 0000000..b96eab0 --- /dev/null +++ b/tests/test_deco.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + + +from pyrimidine.deco import * + + +class TestDeco: + + def test_cache(self): + + @add_cache(attrs=('f',), methods=('bar',)) + class C: + v = 1 + def _f(self): + return self.v + + def bar(self): + pass + + c = C() + + assert c._f() == 1 + assert c.f == 1 + assert c._cache['f'] == 1 + c.v = 2 + assert c._f() == 2 + c.bar() + assert c.f == 2 diff --git a/tests/test_pso.py b/tests/test_pso.py index b06cdd1..3da072c 100644 --- a/tests/test_pso.py +++ b/tests/test_pso.py @@ -6,12 +6,14 @@ from pyrimidine.pso import Particle, ParticleSwarm from pyrimidine.benchmarks.special import rosenbrock + def evaluate(x): return - rosenbrock(x) -class TestPSO(unittest.TestCase): - def setUp(self): +class TestPSO(): + + def test_pso(self): # generate a knapsack problem randomly class _Particle(Particle): @@ -25,8 +27,9 @@ class MyParticleSwarm(ParticleSwarm, BasePopulation): element_class = _Particle default_size = 10 - self.ParticleSwarm = MyParticleSwarm + ParticleSwarm = MyParticleSwarm + + pop = ParticleSwarm.random() + pop.transition() + assert isinstance(pop, ParticleSwarm) - def test_pso(self): - pop = self.ParticleSwarm.random() - data = pop.transition() diff --git a/tests/test_utils.py b/tests/test_utils.py index 2f97494..48ab936 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ from pyrimidine.utils import * -class TestUtils(unittest.TestCase): +class TestUtils: def test_pattern(self): assert pattern([[0,1,1],[1,1,0]]) == '*1*' diff --git a/travis.yml b/travis.yml new file mode 100644 index 0000000..2af55be --- /dev/null +++ b/travis.yml @@ -0,0 +1,20 @@ +language: python + +python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + +install: pip install numpy + +before_script: + - pip install pytest + - pip install poetry + +script: + - poetry build + - pytest test.py + +notifications: + email: false