{"id":21850593,"url":"https://github.com/freakwill/pyrimidine","last_synced_at":"2025-04-14T15:03:09.848Z","repository":{"id":62583126,"uuid":"296263403","full_name":"Freakwill/pyrimidine","owner":"Freakwill","description":"🧬OO implement of genetic algorithm by python","archived":false,"fork":false,"pushed_at":"2024-11-26T10:42:53.000Z","size":23225,"stargazers_count":3,"open_issues_count":6,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-11-26T11:40:17.134Z","etag":null,"topics":["genetic-algorithm","iteration","pyrimidine","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Freakwill.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-17T08:18:49.000Z","updated_at":"2024-11-26T10:42:56.000Z","dependencies_parsed_at":"2022-11-03T21:34:28.876Z","dependency_job_id":"c371f8dd-85eb-4116-acc4-5124484ee4e6","html_url":"https://github.com/Freakwill/pyrimidine","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Freakwill%2Fpyrimidine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Freakwill%2Fpyrimidine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Freakwill%2Fpyrimidine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Freakwill%2Fpyrimidine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Freakwill","download_url":"https://codeload.github.com/Freakwill/pyrimidine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226837173,"owners_count":17689942,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["genetic-algorithm","iteration","pyrimidine","python"],"created_at":"2024-11-28T00:18:20.012Z","updated_at":"2025-04-14T15:03:09.841Z","avatar_url":"https://github.com/Freakwill.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyrimidine\n\n[![License](https://img.shields.io/badge/License-MIT-f5de53?\u0026color=f5de53)](https://github.com/Freakwill/pyrimidine/blob/master/LICENSE)\n\n\n`pyrimidine` is an extensible framework of genetic/evolutionary algorithm by Python. See [pyrimidine's documentation](https://pyrimidine.readthedocs.io/) for more details.\n\n![LOGO](small-logo.png)\n\n## Why\n\n\u003e -- Why is the package named as “pyrimidine”? \n\u003e\n\u003e -- Because it begins with “py”.\n\u003e -- Are you kidding?\n\u003e -- No, I am serious.\n\nIf you have more questions, then log in [google group](https://groups.google.com/g/pyrimidine) and post your questions.\n\n\n## Download\n\nIt has been uploaded to [pypi](https://pypi.org/project/pyrimidine/), so download it with `pip install pyrimidine`, and also could download it from Github.\n\n## Video tutorials\n\n[An gentle introduction](https://www.youtube.com/watch?v=uVf3y427ei4\u0026t=3s)\n\n## Idea\n\nWe view the population as a container of individuals, each individual as a container of chromosomes, and a chromosome as a container (array) of genes. This container could be represented as a list or an array. The Container class has an attribute `element_class`, which specifies the class of the elements within it.\n\nFollowing is the part of the source code of `BaseIndividual` and `BasePopulation`.\n\n```python\nclass BaseIndividual(FitnessModel, metaclass=MetaContainer):\n    element_class = BaseChromosome\n    default_size = 1\n\nclass BasePopulation(PopulationModel, metaclass=MetaContainer):\n    element_class = BaseIndividual\n    default_size = 20\n```\n\nThere are two main kinds of containers: list-like and tuple-like. See the following examples.\n\n```python\n# individual with chromosomes of type _Chromosome\n_Individual1 = BaseIndividual[_Choromosome]\n# individual with 2 chromosomes of type _Chromosome1 and _Chromosome2 respectively\n_Individual2 = MixedIndividual[_Chromosome1, _Chromosome2]\n```\n\n### Math expression\n\n$s$ of type $S$ is a container of $a:A$, represented as follows:\n\n```\ns = {a:A}:S  or  s: S[A]\n```\n\nWe could define a population as a container of individuals or chromosomes, and an individual is a container of chromosomes.\n\nAlgebraically, an individual with one chromosome is equivalent to a chromosome mathematically. A population could also be a container of chromosomes. If the individual has only one chromosome, then just build the population based on chromosomes directly.\n\nThe methods are the functions or operators defined on $s$.\n\n## Use\n\n### Main classes\n\n- BaseGene: the gene of chromosome\n- BaseChromosome: sequence of genes, represents part of a solution (or an entire solution)\n- BaseIndividual: sequence of chromosomes, represents a solution of a problem\n- BasePopulation: a container of individuals, represents a container of a problem\n                also the state of a stochastic process\n- BaseMultipopulation: a container of population for more complicated optimization\n\n\n### import\n\nJust use the command `from pyrimidine import *` to import all of the algorithms.\n\nTo import all algorithms for beginners, simply use the command `from pyrimidine import *`.\n\nTo speed the lib, use the following commands.\n\n```python\nfrom pyrimidine import BaseChromosome, BaseIndividual, BasePopulation # import the base classes form `base.py` to build your own classes\n\n# Commands used frequently\nfrom pyrimidine.base import BinaryChromosome, FloatChromosome # import the Chromosome classes and utilize them directly\n# equivalent to `from pyrimidine import BinaryChromosome, FloatChromosome`\nfrom pyrimidine.population import StandardPopulation, HOFPopulation # For creating population with standard GA \n# the same effect with `from pyrimidine import StandardPopulation, HOFPopulation`\nfrom pyrimidine.indiviual import makeIndividual # a helper to make Individual objects, or `from pyrimidine import makeIndividual`\n\nfrom pyrimidine import MultiPopulation # build the multi-populations\nfrom pyrimidine import MetaContainer # meta class for socalled container class, that is recommended to be used for creating novel evolutionary algorithms.\n\nfrom pyrimidine.deco import fitness_cache, basic_memory # use the cache decorator and memory decorator\n\nfrom pyrimidine import optimize # do optimization implictly with GAs\n\nfrom pyrimidine.pso import Particle, ParticleSwarm # for PSO\nfrom pyrimidine.es import EvolutionStrategy # for ES as a variant of GA\n```\n\nTo import other classes or helpers, please see the docs.\n\n### subclasses\n\n#### Chromosome\n\nGenerally, it is an array of genes.\n\nAs an array of 0-1s, `BinaryChromosome` is used most frequently.\n\n#### Individual\njust subclass `MonoIndividual` in most cases.\n\n```python\nfrom pyrimidine.individual import MonoIndividual\nfrom pyrimidine.chromosome import BinaryChromosome\n\n# or from pyrimidine import MonoIndividual, BinaryChromosome\n\nclass MyIndividual(MonoIndividual):\n    \"\"\"individual with only one chromosome\n    we set the gene to 0 or 1 in the chromosome\n    \"\"\"\n    element_class = BinaryChromosome\n\n    def _fitness(self):\n        ...\n```\n\nSince the helper `makeIndividual(n_chromosomes=1, size=8)` could create such an individual, it is equivalent to\n\n```python\nfrom pyrimidine.individual import makeIndividual\n\nclass MyIndividual(makeIndividual()):\n    # only need to define the fitness\n    def _fitness(self):\n        ...\n```\n\nIf an individual contains several chromosomes, then subclass `MultiIndividual` or `PolyIndividual`. It could be applied in multi-real-variable optimization problems where each variable has a separative binary encoding.\n\nIn most cases, we have to decode chromosomes to real numbers.\n\n```python\nfrom pyrimidine.individual import BaseIndividual\nfrom pyrimidine.chromosome import BinaryChromosome\n\nclass _Chromosome(BinaryChromosome):\n\n    def decode(self):\n        \"\"\"Decode a binary chromosome\n        \n        if the sequence of 0-1 represents a real number, then override the method\n        to transform it to a number\n        \"\"\"\n\nclass ExampleIndividual(BaseIndividual):\n    element_class = _Chromosome\n\n    def _fitness(self):\n        # define the method to calculate the fitness\n        x = self.decode()  # will call decode method of _Chromosome\n        return evaluate(x)\n```\n\nIf the chromosomes in an individual are different with each other, then subclass `MixedIndividual`, meanwhile, the property `element_class` should be assigned with a tuple of classes for each chromosome.\n\n```python\nfrom pyrimidine.individual import MixedIndividual\n\nclass MyIndividual(MixedIndividual):\n    \"\"\"\n    Inherit the fitness from ExampleIndividual directly.\n    It has 6 chromosomes: 5 are instances of _Chromosome, 1 is an instance of FloatChromosome\n    \"\"\"\n    element_class = (_Chromosome,)*5 + (FloatChromosome,)\n```\n\nIt equivalent to `MyIndividual=MixedIndividual[(_Chromosome,)*5 + (FloatChromosome,)]`\n\n#### Population\n\n```python\nfrom pyrimidine.population import StandardPopulation\n\nclass MyPopulation(StandardPopulation):\n    element_class = MyIndividual\n```\n\nIt is equivalent to `MyPopulation = StandardPopulation[MyIndividual]`.\n\n\n### Initialize randomly\n\n`random` is a factory method!\n\nGenerate a population, with 50 individuals and each individual has 100 genes:\n\n`pop = MyPopulation.random(n_individuals=50, size=100)`\n\nWhen each individual contains 5 chromosomes, use\n\n`pop = MyPopulation.random(n_individuals=10, n_chromosomes=5, size=10)`\n\nHowever, we recommand to set `default_size` in the classes, then run `MyPopulation.random()`\n\n```python\nfrom pyrimidine.population import StandardPopulation\n\nclass MyPopulation(StandardPopulation):\n    element_class = MyIndividual // 5\n    default_size = 10\n\n# equiv. to\n\nMyPopulation = StandardPopulation[MyIndividual//5]//10\n```\n\nIn fact, `random` method of `BasePopulation` will call random method of `BaseIndividual`. If you want to generate an individual, then just execute `MyIndividual.random(n_chromosomes=5, size=10)`, or set `default_size`, then execute `MyIndividual.random()`.\n\n### Evolution\n\n#### `evolve` method\nInitialize a population with `random` method, then call `evolve` method.\n\n```python\npop = MyPopulation.random(n_individuals=50, size=100)\npop.evolve()\nprint(pop.solution)\n```\n\nset `verbose=True` to display the data for each generation.\n\n`evolve` method mainly excute two methods: \n- `init`: initial configuration of the algo.\n- `transition`: do each step of the iteration.\n\n#### History\n\nGet the history of the evolution.\n\n```python\nstat={'Mean Fitness':'mean_fitness', 'Best Fitness': lambda pop: pop.best_individual.fitness}\ndata = pop.history(stat=stat)  # use history instead of evolve\n```\n`stat` is a dict mapping keys to function, where string 'mean_fitness' means function `lambda pop:pop.mean_fitness` which gets the mean fitness of the individuals in `pop`. Since we have defined pop.best_individual.fitness as a property, `stat` could be redefined as `{'Fitness': 'fitness', 'Best Fitness': 'max_fitness'}`.\n\nIt requires `ezstat` (optional but recommended), an easy statistical tool developed by the author.\n\n#### performance\n\nUse `pop.perf()` to check the performance, which calls `evolve` several times.\n\n## Example\n\n### Example 1\n\nDescription\n\n    select some of ti, ni, i=1,...,L, ti in {1,2,...,T}, ni in {1,2,...,N}\n    the sum of ni approx. 10, while it does not repeat\n\nThe opt. problem is\n\n    min abs(sum_i{ni}-10) + maximum of frequencies in {ti}\n    where i is selected.\n\n$$\n\\min_I |\\sum_{i\\in I} n_i -10| + t_m\n\\\\\nI\\subset\\{1,\\cdots,L\\}\n$$\nwhere $t_m$ is the mode of $\\{t_i, i\\in I\\}$\n\n```python\nimport numpy as np\n\nt = np.random.randint(1, 5, 100)\nn = np.random.randint(1, 4, 100)\n\nimport collections\n\nfrom pyrimidine.individual import makeBinaryIndividual\nfrom pyrimidine.population import StandardPopulation\n\n\ndef max_repeat(x):\n    # Maximum repetition\n    c = collections.Counter(x)\n    return np.max([b for a, b in c.items()])\n\n\nclass MyIndividual(makeBinaryIndividual()):\n\n    def _fitness(self):\n        x = abs(np.dot(n, self.chromosome)-10)\n        y = max_repeat(ti for ti, c in zip(t, self) if c==1)\n        return - x - y\n\n\nclass MyPopulation(StandardPopulation):\n    element_class = MyIndividual\n\npop = MyPopulation.random(n_individuals=50, size=100)\npop.evolve()\nprint(pop.solution)  # or pop.best_individual.decode()\n```\n\nNote that there is only one chromosome in `MonoIndividual`, which could be got by `self.chromosome`.\n\nIn fact, the population could be the container of chromosomes. Therefore, we can rewrite the classes as follows in a more natural way.\n\n```python\nfrom pyrimidine.chromosome import BinaryChromosome\nfrom pyrimidine.population import StandardPopulation\n\nclass MyChromosome(BinaryChromosome):\n\n    def _fitness(self):\n        x = abs(np.dot(n, self)-10)\n        y = max_repeat(ti for ti, c in zip(t, self) if c==1)\n        return - x - y\n\nclass MyPopulation(StandardPopulation):\n    element_class = MyChromosome\n```\n\nIt is equiv. to\n```python\ndef _fitness(obj):\n    x = abs(np.dot(n, obj)-10)\n    y = max_repeat(ti for ti, c in zip(t, obj) if c==1)\n    return - x - y\n\nMyPopulation = StandardPopulation[BinaryChromosome].set_fitness(_fitness)\n```\n\n### Example2: Knapsack Problem\n\nOne of the famous problem is the knapsack problem. It is a good example for GA.\n\nWe set `history=True` in `evolve` method for the example, that will record the main data of the whole evolution. It will return an object of `pandas.DataFrame`. The argument `stat`  is a dict from a key to function/str(corresponding to a method) representing a mapping from a population to a number. These numbers of one generation will be stored in a row of the dataframe.\n\nsee `# examples/example0`\n\n```python\n#!/usr/bin/env python3\n\nfrom pyrimidine import binaryIndividual, StandardPopulation\nfrom pyrimidine.benchmarks.optimization import *\n\n# generate a knapsack problem randomly\nevaluate = Knapsack.random(n=20)\n\n\nclass MyIndividual(binaryIndividual(size=20)):\n    def _fitness(self):\n        return evaluate(self)\n\nclass MyPopulation(StandardPopulation):\n    element_class = MyIndividual\n    default_size = 10\n\n\npop = MyPopulation.random()\n\nstat={'Mean Fitness':'mean_fitness', 'Best Fitness':'max_fitness'}\ndata = pop.evolve(stat=stat, history=True) # an instance of `pandas.DataFrame`\n\n# Visualization\nimport matplotlib.pyplot as plt\nfig = plt.figure()\nax = fig.add_subplot(111)\ndata[['Mean Fitness', 'Best Fitness']].plot(ax=ax)\nax.set_xlabel('Generations')\nax.set_ylabel('Fitness')\nplt.show()\n```\n\n![plot-history](/Users/william/Programming/myGithub/pyrimidine/plot-history.png)\n\n\n## Extension\n\n`pyrimidine` is extremely extendable. It is easy to implement other iterative models or algorithms, such as simulation annealing(SA) and particle swarm optimization(PSO).\n\nCurrently, it is recommended to define subclasses based on `IterativeModel` as a mixin. (not mandatory)\n\nIn PSO, we regard a particle as an individual, and `ParticleSwarm` as a population. But in the following, we subclass it from `IterativeModel`\n\n```python\nfrom random import random\nfrom operator import attrgetter\n\nimport numpy as np\n\nfrom pyrimidine.base import BaseIndividual\nfrom pyrimidine.chromosome import FloatChromosome\nfrom pyrimidine.mixin import PopulationMixin\nfrom pyrimidine.meta import MetaContainer\nfrom pyrimidine.deco import basic_memory\n\n# pso.py\n@basic_memory\nclass Particle(BaseIndividual):\n    \"\"\"A particle in PSO\n\n    Extends BaseIndividual\n\n    Variables:\n        default_size {number} -- one individual represented by 2 chromosomes: position and velocity\n        phantom {Particle} -- the current state of the particle moving in the solution space.\n    \"\"\"\n\n    element_class = FloatChromosome\n    default_size = 2\n\n    # other methods\n\nclass ParticleSwarm(PopulationMixin):\n    \"\"\"Standard PSO\n    \n    Extends:\n        PopulationMixin\n    \"\"\"\n\n    element_class = Particle\n    default_size = 20\n\n    params = {'learning_factor': 2, 'acceleration_coefficient': 3,\n    'inertia':0.75, 'n_best_particles':0.2, 'max_velocity':None}\n\n    def init(self):\n        for particle in self:\n            particle.init()\n        self.hall_of_fame = self.get_best_individuals(self.n_best_particles, copy=True)\n\n    def update_hall_of_fame(self):\n        hof_size = len(self.hall_of_fame)\n        for ind in self:\n            for k in range(hof_size):\n                if self.hall_of_fame[-k-1].fitness \u003c ind.fitness:\n                    self.hall_of_fame.insert(hof_size-k, ind.copy())\n                    self.hall_of_fame.pop(0)\n                    break\n\n    @property\n    def best_fitness(self):\n        if self.hall_of_fame:\n            return max(map(attrgetter('fitness'), self.hall_of_fame))\n        else:\n            return super().best_fitness\n\n    def transition(self, *args, **kwargs):\n        \"\"\"\n        Transition of the states of particles\n        \"\"\"\n        self.move()\n        self.backup()\n        self.update_hall_of_fame()\n\n    def backup(self):\n        # overwrite the memory of the particle if its current state is better than its memory\n        for particle in self:\n            particle.backup(check=True)\n\n    def move(self):\n        \"\"\"Move the particles\n\n        Define the moving rule of particles, according to the hall of fame and the best record\n        \"\"\"\n\n        scale = random()\n        eta = random()\n        scale_fame = random()\n        for particle in self:\n            for fame in self.hall_of_fame:\n                if particle.fitness \u003c fame.fitness:\n                    particle.update_vilocity_by_fame(fame, scale, scale_fame, \n                        self.inertia, self.learning_factor, self.acceleration_coefficient)\n                    particle.position = particle.position + particle.velocity\n                    break\n        for particle in self.hall_of_fame:\n            particle.update_vilocity(scale, self.inertia, self.learning_factor)\n            particle.position = particle.position + particle.velocity\n```\n\nIf you want to apply PSO, then you can define\n\n```python\nclass MyParticleSwarm(ParticleSwarm, BasePopulation):\n    element_class = _Particle\n    default_size = 20\n\npop = MyParticleSwarm.random()\n```\n\nOf course, it is not mandatory. It is allowed to inherit `ParticleSwarm` from for example `HOFPopulation` directly.\n\n## Related works\n\n| Library   | Design Style      | Versatility | Extensibility | Visualization           |\n|:----------:|:-------|:--------|:--------|:----------|\n| `pyrimidine`| OOP, Meta-programming, Algebra-insprited | Universal | Extensible | export the data in `DataFrame` |\n| `DEAP`     | OOP, Functional, Meta-programming        | Universal | Limited by its philosophy   | export the data in the class `LogBook`  |\n| `gaft`      | OOP, decoration pattern   | Universal | Extensible    | Easy to Implement       |\n| [`geppy`](https://geppy.readthedocs.io/) | based on `DEAP` | Symbolic Regression | Limited | - |\n| [`tpot`](https://github.com/EpistasisLab/tpot) [@olson]/[`gama`](https://github.com/openml-labs/gama) [@pieter]     | [scikit-learn](https://scikit-learn.org/) Style | Hyperparameter Optimization | Limited | -                   |\n| [`gplearn`](https://gplearn.readthedocs.io/)/[`pysr`](https://astroautomata.com/PySR/)   | scikit-learn Style | Symbolic Regression | Limited | -                  |\n| [`scikit-opt`](https://github.com/guofei9987/scikit-opt)| scikit-learn Style | Numerical Optimization | Unextensible | Encapsulated as a data frame      |\n|[`scikit-optimize`](https://scikit-optimize.github.io/stable/)|scikit-learn Style  | Numerical Optimization | Very Limited | provide some plotting function |\n|[`NEAT`](https://neat-python.readthedocs.io/) [@neat-python]| OOP  | Neuroevolution | Limited | use the visualization tools |\n\n: Comparison of the popular genetic algorithm frameworks.\n\n## Contributions\n\nIf you'd like to contribute to `pyrimidine`, please contact me;\nand if you have noticed any bugs, use the [GitHub issues page](https://github.com/Freakwill/pyrimidine/issues) to report them.\n\n\n![LOGO](logo-ai.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreakwill%2Fpyrimidine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffreakwill%2Fpyrimidine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffreakwill%2Fpyrimidine/lists"}