diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29a1faa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Demetry Pasсal, Ryan (Mohammad) Solgi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4b13018 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ + +tag: + git tag $(shell cat version.txt) + git push --tags + +pypipush: + python setup.py develop + python setup.py sdist + python setup.py bdist_wheel + twine upload dist/* --skip-existing diff --git a/README.md b/README.md new file mode 100644 index 0000000..3df5a36 --- /dev/null +++ b/README.md @@ -0,0 +1,1880 @@ +[![PyPI +version](https://badge.fury.io/py/geneticalgorithm2.svg)](https://pypi.org/project/geneticalgorithm2/) +[![Downloads](https://pepy.tech/badge/geneticalgorithm2)](https://pepy.tech/project/geneticalgorithm2) +[![Downloads](https://pepy.tech/badge/geneticalgorithm2/month)](https://pepy.tech/project/geneticalgorithm2) +[![Downloads](https://pepy.tech/badge/geneticalgorithm2/week)](https://pepy.tech/project/geneticalgorithm2) + +[![Gitter](https://badges.gitter.im/geneticalgorithm2/community.svg)](https://gitter.im/geneticalgorithm2/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/PasaOpasen/geneticalgorithm2/pulls) + +https://pasaopasen.github.io/geneticalgorithm2/ + +**geneticalgorithm2** (from [DPEA](https://github.com/PasaOpasen/PasaOpasen.github.io/blob/master/EA_packages.md)) **is the supported advanced optimized fork of non-supported package** [geneticalgorithm](https://github.com/rmsolgi/geneticalgorithm) of *Ryan (Mohammad) Solgi* + +- [About](#about) +- [Installation](#installation) +- [Updates information](#updates-information) + - [**Future**](#future) + - [**TODO firstly**](#todo-firstly) + - [6.8.7 minor update](#687-minor-update) + - [6.8.6 minor update](#686-minor-update) + - [6.8.5 minor update](#685-minor-update) + - [6.8.4 minor update](#684-minor-update) + - [6.8.3 types update](#683-types-update) + - [6.8.2 patch](#682-patch) + - [6.8.1 patch](#681-patch) + - [6.8.0 minor update](#680-minor-update) + - [6.7.7 refactor](#677-refactor) + - [6.7.6 bug fix](#676-bug-fix) + - [6.7.5 refactor](#675-refactor) + - [6.7.4 bug fix](#674-bug-fix) + - [6.7.3 speed up](#673-speed-up) + - [6.7.2 little update](#672-little-update) + - [6.7.1 patch](#671-patch) + - [6.7.0 minor update (new features)](#670-minor-update-new-features) + - [6.6.2 patch (speed up)](#662-patch-speed-up) + - [6.6.1 patch](#661-patch) + - [6.6.0 minor update (refactoring)](#660-minor-update-refactoring) + - [6.5.1 patch](#651-patch) + - [6.5.0 minor update (refactoring)](#650-minor-update-refactoring) + - [6.4.1 patch (bug fix)](#641-patch-bug-fix) + - [6.4.0 minor update (refactoring)](#640-minor-update-refactoring) + - [6.3.0 minor update (refactoring)](#630-minor-update-refactoring) +- [Working process](#working-process) + - [Main algorithm structure](#main-algorithm-structure) + - [How to run](#how-to-run) + - [Constructor parameters](#constructor-parameters) + - [Genetic algorithm's parameters](#genetic-algorithms-parameters) + - [AlgorithmParams object](#algorithmparams-object) + - [Parameters of algorithm](#parameters-of-algorithm) + - [**Count parameters**](#count-parameters) + - [**Crossover**](#crossover) + - [**Mutation**](#mutation) + - [**Selection**](#selection) + - [Methods and Properties of model:](#methods-and-properties-of-model) +- [Examples for beginner](#examples-for-beginner) + - [A minimal example](#a-minimal-example) + - [The simple example with integer variables](#the-simple-example-with-integer-variables) + - [The simple example with Boolean variables](#the-simple-example-with-boolean-variables) + - [The simple example with mixed variables](#the-simple-example-with-mixed-variables) + - [Optimization problems with constraints](#optimization-problems-with-constraints) + - [Middle example: select fixed count of objects from set](#middle-example-select-fixed-count-of-objects-from-set) +- [U should know these features](#u-should-know-these-features) + - [Available crossovers](#available-crossovers) + - [Function timeout](#function-timeout) + - [Standard GA vs. Elitist GA](#standard-ga-vs-elitist-ga) + - [Standard crossover vs. stud EA crossover](#standard-crossover-vs-stud-ea-crossover) + - [Creating better start population](#creating-better-start-population) + - [Select best N of kN](#select-best-n-of-kn) + - [Do local optimization](#do-local-optimization) + - [Optimization with oppositions](#optimization-with-oppositions) + - [Revolutions](#revolutions) + - [Duplicates removing](#duplicates-removing) + - [Cache](#cache) + - [Report checker](#report-checker) + - [Middle callbacks](#middle-callbacks) + - [How to compare efficiency of several versions of GA optimization](#how-to-compare-efficiency-of-several-versions-of-ga-optimization) + - [Hints on how to adjust genetic algorithm's parameters (from `geneticalgorithm` package)](#hints-on-how-to-adjust-genetic-algorithms-parameters-from-geneticalgorithm-package) + - [How to get maximum speed](#how-to-get-maximum-speed) + - [Don't use plotting](#dont-use-plotting) + - [Don't use progress bar](#dont-use-progress-bar) + - [Try to use faster optimizing function](#try-to-use-faster-optimizing-function) + - [Specify custom optimized `mutation`, `crossover`, `selection`](#specify-custom-optimized-mutation-crossover-selection) + - [Specify `fill_children` method](#specify-fill_children-method) +- [Examples pretty collection](#examples-pretty-collection) + - [Optimization test functions](#optimization-test-functions) + - [Sphere](#sphere) + - [Ackley](#ackley) + - [AckleyTest](#ackleytest) + - [Rosenbrock](#rosenbrock) + - [Fletcher](#fletcher) + - [Griewank](#griewank) + - [Penalty2](#penalty2) + - [Quartic](#quartic) + - [Rastrigin](#rastrigin) + - [SchwefelDouble](#schwefeldouble) + - [SchwefelMax](#schwefelmax) + - [SchwefelAbs](#schwefelabs) + - [SchwefelSin](#schwefelsin) + - [Stairs](#stairs) + - [Abs](#abs) + - [Michalewicz](#michalewicz) + - [Scheffer](#scheffer) + - [Eggholder](#eggholder) + - [Weierstrass](#weierstrass) + - [Using GA in reinforcement learning](#using-ga-in-reinforcement-learning) + - [Using GA with image reconstruction by polygons](#using-ga-with-image-reconstruction-by-polygons) +- [Popular questions](#popular-questions) + - [How to disable autoplot?](#how-to-disable-autoplot) + - [How to plot population scores?](#how-to-plot-population-scores) + - [How to specify evaluated function for all population?](#how-to-specify-evaluated-function-for-all-population) + - [What about parallelism?](#what-about-parallelism) + - [How to initialize start population? How to continue optimization with new run?](#how-to-initialize-start-population-how-to-continue-optimization-with-new-run) +# About + +[**geneticalgorithm2**](https://pasaopasen.github.io/geneticalgorithm2/) is very flexible and highly optimized Python library for implementing classic +[genetic-algorithm](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) (GA). + +Features of this package: + +* written on **pure python** +* **extremely fast** +* **no hard dependencies** (only numpy primary, can work without matplotlib) +* **easy to run**: no need to perform long task-setup process +* easy to logging, reach **support of flexible callbacks** +* **many built-in plotting functions** +* **many built-in cases of crossover, mutation and selection** +* support of integer, boolean and real (continuous/discrete) variables types +* support of mixed types of variables +* **support of classic, elitist and studEA genetic algorithm combinations** +* **support of revolutions and duplicates utilization** +* **reach support of customization** + +# Installation + +Install this package with standard dependencies to use the entire functional. +``` +pip install geneticalgorithm2 +``` + +Install this package with full dependencies to use all provided functional. + +``` +pip install geneticalgorithm2[full] +``` + +# Updates information + +## **Future** + +- duplicates removing and revolutions will be moved to `MiddleCallbacks` and removed as alone `run()` parameters +- `function_timeout` and `function` will be moved to `run()` method +- new stop criteria callbacks (min std, max functions evaluations) +- `vartype` will support strings like `iiiiibbf` + +## **TODO firstly** +- Remove old style mensions from README + +## 6.8.7 minor update + +- some code refactor +- fixes: + - ensure the directory of generation file exists on save + +## 6.8.6 minor update + +- small package installation update: add `pip install geneticalgorithm2[full]` version +- small refactor + +## 6.8.5 minor update + +- update `OppOpPopInit` `2.0.0->2.0.1` +- set default `function_timeout` to `None` which means no use of function time checking +- remove `joblib` and `func_timeout` from necessary dependencies + +## 6.8.4 minor update + +- a bit of refactor +- little optimizations +- add empty field `fill_children(pop_matrix, parents_count)` to `geneticalgorithm2` class to specify children creating behavior (what is the most intensive part of algorithm after optimizing func calculations), see [this](#specify-fill_children-method) + +## 6.8.3 types update + +- much more type hints + +## 6.8.2 patch + +- for printing info +- fix logic: now population is always sorted before going to callbacks + +## 6.8.1 patch + +- printing progress bar to `'stderr'` or `'stdout'` or `None` (disable) by choice (`progress_bar_stream` argument of `run()`), deprecated `disable_progress_bar` +- little speed up +- new `geneticalgorithm2.vectorized_set_function` set function, which can be faster for big populations + +## 6.8.0 minor update + +- remove `crossover_probability` model parameter because of it has no sense to exist (and 1.0 value is better than others, take a look at [results](/tests/output/sense_of_crossover_prob__no_sense.png)). This parameter came from `geneticalgorithm` old package and did`t change before. + +## 6.7.7 refactor + +- change some behavior about parents selection + +## 6.7.6 bug fix + +- fix some bug of `variable_type=='bool'` +- some refactor of progress bar +- add some dependencies to `setup.py` + +## 6.7.5 refactor + +- shorter progress bar (length can be controlled by setting `PROGRESS_BAR_LEN` field of `geneticalgorithm2` class) +- shorter logic of `run()`, more informative output + +## 6.7.4 bug fix + +- bug fix + +## 6.7.3 speed up + +- refactor to make `run()` method faster + +## 6.7.2 little update + +- better flexible logic for report, [take a look](#report-checker) +- removed `show mean` parameter from `model.plot_result` and now model reports only best score by default, not average and so on (u can specify if u wanna report average too) +- `plot_several_lines` useful function + +## 6.7.1 patch + +- changes according to new [OppOpPopInit](https://github.com/PasaOpasen/opp-op-pop-init) version + +## 6.7.0 minor update (new features) + +- add `mutation_discrete_type` and `mutation_discrete_probability` parameters in model. It controls mutation behavior for discrete (integer) variables and works like `mutation_type` and `mutation_probability` work for continuous (real) variables. Take a look at [algorithm parameters](#parameters-of-algorithm) + +## 6.6.2 patch (speed up) + +- fix and speed up mutation + +## 6.6.1 patch + +- removed unnecessary dependencies + +## 6.6.0 minor update (refactoring) + +- deprecated `variable_type_mixed`, now use `variable_type` for mixed optimization too +- deprecated `output_dict`, now it's better object with name `result` +- refactor of big part of **tests** +- refactor of README + +## 6.5.1 patch + +- replace `collections.Sequence` with `collections.abc.Sequence`, now it should work for `python3.10+` + +## 6.5.0 minor update (refactoring) + +- another form of data object using with middle callbacks (`MiddleCallbackData` dataclass instead of dictionary) +- type hints for callbacks module + +## 6.4.1 patch (bug fix) + +- fix bug setting attribute to algorithm parameters (in middle callbacks) + + +## 6.4.0 minor update (refactoring) + +- new valid forms for `start_generation`; now it's valid to use + * `None` + * `str` path to saved generation + * dictionary with structure `{'variables': variables/None, 'scores': scores/None}` + * `Generation` object: `Generation(variables = variables, scores = scores)` + * `np.ndarray` with shape `(samples, dim)` for only population or `(samples, dim+1)` for concatenated population and score (scores is the last matrix column) + * `tuple(np.ndarray/None, np.ndarray/None)` for variables and scores + + here `variables` is 2D numpy array with shape `(samples, dim)`, `scores` is 1D numpy array with scores (function values) for each sample; [here](tests/output/start_gen.py) and [here](#how-to-initialize-start-population-how-to-continue-optimization-with-new-run) u can see examples of using these valid forms + + +## 6.3.0 minor update (refactoring) + +- type hints for entire part of functions +- new valid forms for function parameters (now u don't need to use numpy arrays everywhere) +- `AlgorithmParams` class for base GA algorithm parameters (instead of dictionary) +- `Generation` class for saving/loading/returning generation (instead of dictionary) + +All that classes are collected [in file](geneticalgorithm2/classes.py). To maintain backward compatibility, `AlgorithmParams` and `Generation` classes have dictionary-like interface for getting fields: u can use `object.field` or `object['field']` notations. + + +# Working process + +## Main algorithm structure + +``` +Pre-process: making inner functions depends on params, making/loading start population + +while True: + + if reason to stop (time is elapsed / no progress / generation count is reached / min value is reached): + break + + + select parents to crossover from last population and put them to new population: + select (elit count) best samples + select (parents count - elit count) random samples (by selection function) + + create (total samples count - parents count) children (samples from selected parents) and put them to new population: + while not all children are created: + select 2 random parents + make child1, child2 from them using crossover + mutate child1 by mutation (model.mut) + mutate child2 by middle mutation (model.mut_middle) + put children to new population + + remove duplicates, make revolutions, sort population by scores + use callbacks, use middle callbacks + +Post-process: plotting results, saving + +``` + +## How to run + +Firstly, u should **import needed packages**. All available (but not always necessary) imports are: + +```python +import numpy as np + +# the only one required import +from geneticalgorithm2 import geneticalgorithm2 as ga # for creating and running optimization model + +from geneticalgorithm2 import Generation, AlgorithmParams # classes for comfortable parameters setting and getting + +from geneticalgorithm2 import Crossover, Mutations, Selection # classes for specific mutation and crossover behavior + +from geneticalgorithm2 import Population_initializer # for creating better start population + +from geneticalgorithm2 import np_lru_cache # for cache function (if u want) + +from geneticalgorithm2 import plot_pop_scores # for plotting population scores, if u want + +from geneticalgorithm2 import Callbacks # simple callbacks (will be deprecated) + +from geneticalgorithm2 import Actions, ActionConditions, MiddleCallbacks # middle callbacks +``` + +Next step: **define minimized function** like + +```python +def function(X: np.ndarray) -> float: # X as 1d-numpy array + return np.sum(X**2) + X.mean() + X.min() + X[0]*X[2] # some float result +``` + +If u want to find *maximum*, use this idea: + +```python +f_tmp = lambda arr: -target(arr) + +# +# ... find global min +# + +target_result = -global_min +``` + +Okay, also u should **create the bounds for each variable** (if exist) like here: + +```python +var_bound = np.array([[0,10]]*3) # 2D numpy array with shape (dim, 2) + +# also u can use Sequence of Tuples (from version 6.3.0) +var_bound = [ + (0, 10), + (0, 10), + (0, 10) +] + +``` +U don't need to use variable boundaries only if variable type of each variable is boolean. This case will be converted to discret variables with bounds `(0, 1)`. + +After that **create a `geneticalgorithm2` (was imported early as ga) object**: + +```python +# style before 6.3.0 version (but still works) +model = ga( + function, + dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + function_timeout = 10, + algorithm_parameters={ + 'max_num_iteration': None, + 'population_size':100, + 'mutation_probability': 0.1, + 'mutation_discrete_probability': None, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'mutation_discrete_type': 'uniform_discrete', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None + } +) + +# from version 6.3.0 it is equal to + +model = ga( + function, + dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + function_timeout = 10, + algorithm_parameters=AlgorithmParams( + max_num_iteration = None, + population_size = 100, + mutation_probability = 0.1, + mutation_discrete_probability = None, + elit_ratio = 0.01, + parents_portion = 0.3, + crossover_type = 'uniform', + mutation_type = 'uniform_by_center', + mutation_discrete_type = 'uniform_discrete', + selection_type = 'roulette', + max_iteration_without_improv = None + ) +) + +# or (with defaults) +model = ga( + function, dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + function_timeout = 10, + algorithm_parameters=AlgorithmParams() +) + +``` + +**Run the search method**: + +```python +# all of this parameters are default +result = model.run( + no_plot = False, + progress_bar_stream = 'stdout', + disable_printing = False, + + set_function = None, + apply_function_to_parents = False, + start_generation = None, + studEA = False, + mutation_indexes = None, + + init_creator = None, + init_oppositors = None, + duplicates_oppositor = None, + remove_duplicates_generation_step = None, + revolution_oppositor = None, + revolution_after_stagnation_step = None, + revolution_part = 0.3, + + population_initializer = Population_initializer(select_best_of = 1, local_optimization_step = 'never', local_optimizer = None), + + stop_when_reached = None, + callbacks = [], + middle_callbacks = [], + time_limit_secs = None, + save_last_generation_as = None, + seed = None +) + +# best solution +print(result.variable) + +# best score +print(result.score) + +# last generation +print(result.last_generation) + +``` + +## Constructor parameters + +* **function** (`Callable[[np.ndarray], float]`) - the given objective function to be minimized +NOTE: This implementation minimizes the given objective function. (For maximization multiply function by a negative sign: the absolute value of the output would be the actual objective function) + +* **dimension** (`int`) - the number of decision variables + +* **variable_type** (`Union[str, Sequence[str]]`) - 'bool' if all variables are Boolean; 'int' if all variables are integer; and 'real' if all variables are real value or continuous. For mixed types use sequence of string of type for each variable + +* **variable_boundaries** (`Optional[Union[np.ndarray, Sequence[Tuple[float, float]]]]`) - Default None; leave it None if variable_type is 'bool'; otherwise provide an sequence of tuples of length two as boundaries for each variable; the length of the array must be equal dimension. +For example, `np.array([[0,100],[0,200]])` or `[(0, 100), (0, 200)]` determines lower boundary 0 and upper boundary 100 for first and upper boundary 200 for second variable where dimension is 2. + +* **function_timeout** (`float`) - if the given function does not provide +output before function_timeout (unit is seconds) the algorithm raise error. +For example, when there is an infinite loop in the given function. `None` means disabling + +* **algorithm_parameters** (`Union[AlgorithmParams, Dict[str, Any]]`). Dictionary or AlgorithmParams object with fields: + * @ **max_num_iteration** (`int/None`) - stopping criteria of the genetic algorithm (GA) + * @ **population_size** (`int > 0`) + * @ **mutation_probability** (`float in [0,1]`) + * @ **mutation_discrete_probability** (`float in [0,1]` or `None`) + * @ **elit_ration** (`float in [0,1]`) - part of elit objects in population; if > 0, there always will be 1 elit object at least + * @ **parents_portion** (`float in [0,1]`) - part of parents from previous population to save in next population (including `elit_ration`) + * @ **crossover_type** (`Union[str, Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]]`) - Default is `uniform`. + * @ **mutation_type** (`Union[str, Callable[[float, float, float], float]]`) - Default is `uniform_by_center` + * @ **mutation_discrete_type** (`Union[str, Callable[[int, int, int], int]]`) - Default is `uniform_discrete` + * @ **selection_type** (`Union[str, Callable[[np.ndarray, int], np.ndarray]]`) - Default is `roulette` + * @ **max_iteration_without_improv** (`int/None`) - maximum number of successive iterations without improvement. If `None` it is ineffective + +## Genetic algorithm's parameters + +### AlgorithmParams object + +The parameters of GA is defined as a dictionary or `AlgorithmParams` object: + +```python + +algorithm_param = AlgorithmParams( + max_num_iteration = None, + population_size = 100, + mutation_probability = 0.1, + mutation_discrete_probability = None, + elit_ratio = 0.01, + parents_portion = 0.3, + crossover_type = 'uniform', + mutation_type = 'uniform_by_center', + mutation_discrete_type = 'uniform_discrete', + selection_type = 'roulette', + max_iteration_without_improv = None + ) + + +# old style with dictionary +# sometimes it's easier to use this style +# especially if u need to set only few params +algorithm_param = { + 'max_num_iteration': None, + 'population_size':100, + 'mutation_probability': 0.1, + 'mutation_discrete_probability': None, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'mutation_discrete_type': 'uniform_discrete', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None + } + +``` + +To get actual default params use code: +```python +params = ga.default_params +``` + +To get actual parameters of existing model use code: +```python +params = model.param +``` + +An example of setting a new set of parameters for genetic algorithm and running `geneticalgorithm2` for our first simple example again: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound=[(0,10)]*3 + +algorithm_param = {'max_num_iteration': 3000, + 'population_size':100, + 'mutation_probability': 0.1, + 'mutation_discrete_probability': None, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'mutation_discrete_type': 'uniform_discrete', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None} + +model=ga(function=f, + dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=algorithm_param + ) + +model.run() +``` +**Important**. U may use the small dictionary with only important parameters; other parameters will be default. It means the dictionary +```js +algorithm_param = {'max_num_iteration': 150, + 'population_size':1000} +``` +is equal to: +```js +algorithm_param = {'max_num_iteration': 150, + 'population_size':1000, + 'mutation_probability': 0.1, + 'mutation_discrete_probability': None, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'mutation_discrete_type': 'uniform_discrete', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None} +``` + +But it is better to use `AlgorithmParams` object instead of dictionaries. + +### Parameters of algorithm + +#### **Count parameters** + +* **max_num_iteration**: The termination criterion of GA. +If this parameter's value is `None` the algorithm sets maximum number of iterations automatically as a function of the dimension, boundaries, and population size. The user may enter any number of iterations that they want. It is highly recommended that the user themselves determines the **max_num_iterations** and not to use `None`. Notice that **max_num_iteration** has been changed to 3000 (it was already `None`). + +* **population_size**: determines the number of trial solutions in each iteration. + +* **elit_ration**: determines the number of elites in the population. The default value is 0.01 (i.e. 1 percent). For example when population size is 100 and **elit_ratio** is 0.01 then there is one elite unit in the population. If this parameter is set to be zero then `geneticalgorithm2` implements a standard genetic algorithm instead of elitist GA. [See example](#standard-ga-vs-elitist-ga) of difference + +* **parents_portion**: the portion of population filled by the members of the previous generation (aka parents); default is 0.3 (i.e. 30 percent of population) + +* **max_iteration_without_improv**: if the algorithms does not improve the objective function over the number of successive iterations determined by this parameter, then GA stops and report the best found solution before the `max_num_iterations` to be met. The default value is `None`. + +#### **Crossover** + +* **crossover_type**: there are several options including `'one_point'`, `'two_point'`, `'uniform'`, `'segment'`, `'shuffle'` crossover functions; default is `'uniform'` crossover. U also can use crossover as functions from `Crossover` class: + * `Crossover.one_point()` + * `Crossover.two_point()` + * `Crossover.uniform()` + * `Crossover.uniform_window(window = 7)` + * `Crossover.shuffle()` + * `Crossover.segment()` + * `Crossover.mixed(alpha = 0.5)` -- only for real variables + * `Crossover.arithmetic()` -- only for real variables + + Have a look at [crossovers' logic](#available-crossovers) + + If u want, write your own crossover function using simple syntax: + ```python + def my_crossover(parent_a: np.ndarray, parent_b: np.ndarray): + # some code + return child_1, child_2 + ``` + +#### **Mutation** + +* **mutation_probability**: determines the chance of each gene in each individual solution to be replaced by a random value. Works for continuous variables or for all variables if **mutation_discrete_probability** is `None` + +* **mutation_discrete_probability**: works like **mutation_probability** but for discrete variables. If `None`, will be changed to **mutation_probability** value; so just don't specify this parameter if u don't need special mutation behavior for discrete variables + +* **mutation_type**: there are several options (only for real variables) including `'uniform_by_x'`, `'uniform_by_center'`, `'gauss_by_x'`, `'gauss_by_center'`; default is `'uniform_by_center'`. U also can use mutation as functions from `Mutations` class: + * `Mutations.gauss_by_center(sd = 0.2)` + * `Mutations.gauss_by_x(sd = 0.1)` + * `Mutations.uniform_by_center()` + * `Mutations.uniform_by_x()` + + (If u want) write your mutation function using syntax: + ```python + def my_mutation(current_value: float, left_border: float, right_border: float) -> float: + # some code + return new_value + ``` + +* **mutation_discrete_type**: now there is only one option for discrete variables mutation: `uniform_discrete` (`Mutations.uniform_discrete()`) which works like `uniform_by_center` real mutation but with integer numbers. Anyway, this option was included at version 6.7.0 to support custom discrete mutations if u need it. For using custom mutation just set this parameter to function like + ```python + def my_mutation(current_value: int, left_border: int, right_border: int) -> int: + # some code + return new_value + ``` + +#### **Selection** + +* **selection_type**: there are several options (only for real) including `'fully_random'` (just for fun), `'roulette'`, `'stochastic'`, `'sigma_scaling'`, `'ranking'`, `'linear_ranking'`, `'tournament'`; default is `roulette`. U also can use selection as functions from `Selection` class: + * `Selection.fully_random()` + * `Selection.roulette()` + * `Selection.stochastic()` + * `Selection.sigma_scaling(epsilon = 0.05)` + * `Selection.ranking()` + * `Selection.linear_ranking(selection_pressure = 1.5)` + * `Selection.tournament(tau = 2)` + + If u want, write your selection function using syntax: + ```python + def my_mutation(sorted_scores: np.ndarray, parents_count: int): + # some code + return array_of_parents_indexes + ``` +![](tests/output/selections.png) + +## Methods and Properties of model: + +The main method if **run()**. It has parameters: + +* **no_plot** (`bool`) - do not plot results using matplotlib by default + +* **progress_bar_stream** (`Optional[str]`) - `'stdout'` to print progress bar to `stdout`, `'stderr'` for `stderr`, `None` to disable progress bar (also it can be faster by 10-20 seconds) + +* **disable_printing** (`bool`) - don't print any text (except progress bar) + +* **set_function** (`Optional[Callable[[np.ndarray], np.ndarray]]`): 2D-array -> 1D-array function, which applies to matrix of population (size (samples, dimension)) to estimate their values ("scores" in some sense) + +* **apply_function_to_parents** (`bool`) - apply function to parents from previous generation (if it's needed), it can be needed at working with games agents, but for other tasks will just waste time + +* **start_generation** (`Union[str, Dict[str, np.ndarray], Generation, np.ndarray, Tuple[Optional[np.ndarray], Optional[np.ndarray]]]`) -- one of cases ([take a look](#how-to-initialize-start-population-how-to-continue-optimization-with-new-run)): + * `Generation` object + * dictionary with structure `{'variables':2D-array of samples, 'scores': function values on samples}` (if `'scores'` value is `None` the scores will be compute) + * path to `.npz` file (`str`) with saved generation + * `np.ndarray` (with shape `(samples, dim)` or `(samples, dim+1)`) + * tuple of `np.ndarray`s / `None`. + +* **studEA** (`bool`) - using stud EA strategy (crossover with best object always). Default is false. [Take a look](#standard-crossover-vs-stud-ea-crossover) +* **mutation_indexes** (`Optional[Union[Sequence[int], Set[int]]]`) - indexes of dimensions where mutation can be performed (all dimensions by default). [Example](tests/mut_indexes.py) + +* **init_creator**: (`Optional[Callable[[], np.ndarray]]`), the function creates population samples. By default -- random uniform for real variables and random uniform for int. [Example](#optimization-with-oppositions) +* **init_oppositors**: (`Optional[Sequence[Callable[[np.ndarray], np.ndarray]]]`) -- the list of oppositors creates oppositions for base population. No by default. [Example](#optimization-with-oppositions) +* **duplicates_oppositor**: `Optional[Callable[[np.ndarray], np.ndarray]]`, oppositor for applying after duplicates removing. By default -- using just random initializer from creator. [Example](#duplicates-removing) +* **remove_duplicates_generation_step**: `None/int`, step for removing duplicates (have a sense with discrete tasks). No by default. [Example](#duplicates-removing) +* **revolution_oppositor** = `Optional[Callable[[np.ndarray], np.ndarray]]`, oppositor for revolution time. No by default. [Example](#revolutions) +* **revolution_after_stagnation_step** = `None/int`, create revolution after this generations of stagnation. No by default. [Example](#revolutions) +* **revolution_part** (`float`): the part of generation to being oppose. By default is 0.3. [Example](#revolutions) + +* **population_initializer** (`Tuple[int, Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]]`) -- object for actions at population initialization step to create better start population. [Take a look](#creating-better-start-population) + +* **stop_when_reached** (`Optional[float]`) -- stop searching after reaching this value (it can be potential minimum or something else) + +* **callbacks** (`Optional[Sequence[Callable[[int, List[float], np.ndarray, np.ndarray], None]]]`) - list of callback functions with structure: + ```python + def callback(generation_number, report_list, last_population_as_2D_array, last_population_scores_as_1D_array): + # + # do some action + # + ``` + See [example of using callbacks](tests/callbacks.py). There are several callbacks in `Callbacks` class, such as: + * `Callbacks.SavePopulation(folder, save_gen_step = 50, file_prefix = 'population')` + * `Callbacks.PlotOptimizationProcess(folder, save_gen_step = 50, show = False, main_color = 'green', file_prefix = 'report')` + +* **middle_callbacks** (`Sequence`) - list of functions made `MiddleCallbacks` class (large opportunity, please, have a look at [this](#middle-callbacks)) + + +* **time_limit_secs** (`Optional[float]`) - limit time of working (in seconds). If `None`, there is no time limit (limit only for count of generation and so on). See [little example of using](tests/time_limit.py). Also there is simple conversion function for conversion some time in seconds: + ```python + from truefalsepython import time_to_seconds + + total_seconds = time_to_seconds( + days = 2, # 2 days + hours = 13, # plus 13 hours + minutes = 7, # plus 7 minutes + seconds = 44 # plus 44 seconds + ) + ``` + +* **save_last_generation_as** (`Optional[str]`) - path to `.npz` file for saving last_generation as numpy dictionary like `{'population': 2D-array, 'scores': 1D-array}`, `None` if doesn't need to save in file; [take a look](#how-to-initialize-start-population-how-to-continue-optimization-with-new-run) + +* **seed** (`Optional[int]`) - random seed (None is doesn't matter) + +It would be more logical to use params like `studEA` as an algorithm param, but `run()`-way can be more comfortable for real using. + + +**output**: + +* `result`: is a wrap on last generation with fields: + * `last_generation` -- `Generation` object of last generation + * `variable` -- best unit of last generation + * `score` -- metric of the best unit + +* `report`: is a record of the progress of the algorithm over iterations. Also u can specify to report not only best values. [Go to](#report-checker) + + + + +# Examples for beginner + +## A minimal example +Assume we want to find a set of `X = (x1,x2,x3)` that minimizes function `f(X) = x1 + x2 + x3` where `X` can be any real number in `[0, 10]`. + +This is a trivial problem and we already know that the answer is `X = (0,0,0)` where `f(X) = 0`. + +We just use this simple example to see how to implement geneticalgorithm2. First we import geneticalgorithm2 and [numpy](https://numpy.org). Next, we define +function `f` which we want to minimize and the boundaries of the decision variables. Then simply geneticalgorithm2 is called to solve the defined optimization problem as follows: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*3 + +model = ga(function=f, dimension=3, variable_type='real', variable_boundaries=varbound) + +model.run() +``` + + +**geneticalgorithm2 has some arguments**: +1. Obviously the first argument is the function `f` we already defined. +2. Our problem has three variables so we set dimension equal `3`. +3. Variables are real (continuous) so we use string `'real'` to notify the type of +variables (geneticalgorithm2 accepts other types including boolean, integers and +mixed; see other examples). +1. Finally, we input `varbound` which includes the boundaries of the variables. +Note that the length of variable_boundaries must be equal to dimension. + +If you run the code, you should see a progress bar that shows the progress of the +genetic algorithm (GA) and then the solution, objective function value and the convergence curve as follows: + +![](https://github.com/PasaOpasen/geneticalgorithm2/blob/master/genetic_algorithm_convergence.gif) + +Also we can access to the best answer of the defined optimization problem found by GA as a dictionary and a report of the progress of the genetic algorithm. +To do so we complete the code as follows: + +```python +convergence = model.report + +solution = model.result +``` + +## The simple example with integer variables + +Considering the problem given in the simple example above. +Now assume all variables are integers. So `x1, x2, x3` can be any integers in `[0, 10]`. +In this case the code is as the following: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*3 + +model = ga(function=f, dimension=3, variable_type='int', variable_boundaries=varbound) + +model.run() +``` +So, as it is seen the only difference is that for `variable_type` we use string `'int'`. + +## The simple example with Boolean variables + +Considering the problem given in the simple example above. +Now assume all variables are boolean instead of real or integer. So `X` can be either zero or one. Also instead of three let's have 30 variables. +In this case the code is as the following: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +model = ga(function=f, dimension=30, variable_type='bool') + +model.run() +``` + +Note for variable_type we use string `'bool'` when all variables are boolean. +Note that when variable_type equal `'bool'` there is no need for `variable_boundaries` to be defined. + +## The simple example with mixed variables + +Considering the problem given in the the simple example above where we want to minimize `f(X) = x1 + x2 + x3`. +Now assume `x1` is a real (continuous) variable in `[0.5,1.5]`, `x2` is an integer variable in `[1,100]`, and `x3` is a boolean variable that can be either zero or one. +We already know that the answer is `X = (0.5,1,0)` where `f(X) = 1.5` +We implement geneticalgorithm2 as the following: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +varbound = [[0.5,1.5],[1,100],[0,1]] +vartype = ('real', 'int', 'int') +model = ga(function=f, dimension=3, variable_type=vartype, variable_boundaries=varbound) + +model.run() +``` + +## Optimization problems with constraints +In all above examples, the optimization problem was unconstrained. Now consider that we want to minimize `f(X) = x1+x2+x3` where `X` is a set of real variables in `[0, 10]`. Also we have an extra constraint so that sum of `x1` and `x2` is equal or greater than 2. The minimum of `f(X)` is 2. +In such a case, a trick is to define penalty function. Hence we use the code below: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + pen=0 + if X[0]+X[1]<2: + pen=500+1000*(2-X[0]-X[1]) + return np.sum(X)+pen + +varbound=[[0,10]]*3 + +model=ga(function=f,dimension=3,variable_type='real',variable_boundaries=varbound) + +model.run() + +``` +As seen above we add a penalty to the objective function whenever the constraint is not met. + +Some hints about how to define a penalty function: + +1. Usually you may use a constant greater than the maximum possible value of the objective function if the maximum is known or if we have a guess of that. Here the highest possible value of our function is 300 (i.e. if all variables were 10, `f(X)=300`). So I chose a constant of 500. So, if a trial solution is not in the feasible region even though its objective function may be small, the penalized objective function (fitness function) is worse than any feasible solution. +2. Use a coefficient big enough and multiply that by the amount of violation. This helps the algorithm learn how to approach feasible domain. +3. How to define penalty function usually influences the convergence rate of an evolutionary algorithm. In my [book on metaheuristics and evolutionary algorithms](https://www.wiley.com/en-us/Meta+heuristic+and+Evolutionary+Algorithms+for+Engineering+Optimization-p-9781119386995) you can learn more about that. +4. Finally after you solved the problem test the solution to see if boundaries are met. If the solution does not meet constraints, it shows that a bigger penalty is required. However, in problems where optimum is exactly on the boundary of the feasible region (or very close to the constraints) which is common in some kinds of problems, a very strict and big penalty may prevent the genetic algorithm to approach the optimal region. In such a case designing an appropriate penalty function might be more challenging. Actually what we have to do is to design a penalty function that let the algorithm searches unfeasible domain while finally converge to a feasible solution. Hence you may need more sophisticated penalty functions. But in most cases the above formulation work fairly well. + +## Middle example: select fixed count of objects from set + +For some task u need to think a lot and create good specific crossover or mutation functions. For example, take a look at this problem: + + From set like X = {x1, x2, x3, ..., xn} u should select only k objects which get the best function value + +U can do it using this code: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + + +subset_size = 20 # how many objects we can choose + +objects_count = 100 # how many objects are in set + +my_set = np.random.random(objects_count)*10 - 5 # set values + +# minimized function +def f(X): + return abs(np.mean(my_set[X==1]) - np.median(my_set[X==1])) + +# initialize start generation and params + +N = 1000 # size of population +start_generation = np.zeros((N, objects_count)) +indexes = np.arange(0, objects_count, dtype = np.int8) # indexes of variables + +for i in range(N): + inds = np.random.choice(indexes, subset_size, replace = False) + start_generation[i, inds] = 1 + + +def my_crossover(parent_a, parent_b): + a_indexes = set(indexes[parent_a == 1]) + b_indexes = set(indexes[parent_b == 1]) + + intersect = a_indexes.intersection(b_indexes) # elements in both parents + a_only = a_indexes - intersect # elements only in 'a' parent + b_only = b_indexes - intersect + + child_inds = np.array(list(a_only) + list(b_only), dtype = np.int8) + np.random.shuffle(child_inds) # mix + + children = np.zeros((2, parent_a.size)) + if intersect: + children[:, np.array(list(intersect))] = 1 + children[0, child_inds[:int(child_inds.size/2)]] = 1 + children[1, child_inds[int(child_inds.size/2):]] = 1 + + return children[0,:], children[1,:] + + +model = ga(function=f, + dimension=objects_count, + variable_type='bool', + algorithm_parameters={ + 'max_num_iteration': 500, + 'mutation_probability': 0, # no mutation, just crossover + 'elit_ratio': 0.05, + 'parents_portion': 0.3, + 'crossover_type': my_crossover, + 'max_iteration_without_improv': 20 + } + ) + +model.run(no_plot = False, start_generation=(start_generation, None)) +``` + +# U should know these features + +## Available crossovers + +For two example parents (*one with ones* and *one with zeros*) next crossovers will give same children ([examples](tests/crossovers_examples.py)): + +* **one_point**: + +|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0| + +* **two_point**: + +|1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0| + +* **uniform**: + +|1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 1| + +* **uniform_window**: + +|1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0| + +* **shuffle**: + +|0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1| + +* **segment**: + +|0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0| + +* **arithmetic**: + +|0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13 | 0.13| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87 | 0.87| + +* **mixed**: + +|0.63 | 0.84 | 1.1 | 0.73 | 0.67 | -0.19 | 0.3 | 0.72 | -0.18 | 0.61 | 0.84 | 1.14 | 1.36 | -0.37 | -0.19| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0.51 | 0.58 | 0.43 | 0.42 | 0.55 | 0.49 | 0.57 | 0.48 | 0.46 | 0.56 | 0.56 | 0.54 | 0.44 | 0.51 | 0.4| + + +## Function timeout + +**geneticalgorithm2** is designed such that if the given function does not provide +any output before timeout (the default value is 10 seconds), the algorithm +would be terminated and raise the appropriate error. + +In such a case make sure the given function +works correctly (i.e. there is no infinite loop in the given function). Also if the given function takes more than 10 seconds to complete the work +make sure to increase function_timeout in arguments. + +## Standard GA vs. Elitist GA + +The convergence curve of an elitist genetic algorithm is always non-increasing. So, the best ever found solution is equal to the best solution of the last iteration. However, the convergence curve of a standard genetic algorithm is different. If `elit_ratio` is zero geneticalgorithm2 implements a standard GA. The output of geneticalgorithm2 for standard GA is the best ever found solution not the solution of the last iteration. The difference between the convergence curve of standard GA and elitist GA is shown below: + +![](tests/output/standard_vs_elitist.png) + +## Standard crossover vs. stud EA crossover + +[Stud EA](https://link.springer.com/chapter/10.1007%2FBFb0056910) is the idea of using crossover always with best object. So one of two parents is always the best object of population. It can help us in a lot of tasks! + +![](tests/output/studEA.png) + +## Creating better start population + +There is `Population_initializer(select_best_of = 4, local_optimization_step = 'never', local_optimizer = None)` object for creating better start population. It has next arguments: + +* `select_best_of` (`int`) -- select `1/select_best_of` best part of start population. For example, for `select_best_of = 4` and `population_size = N` will be selected N best objects from 5N generated objects (if `start_generation = None`). If `start_generation` is not None, it will be selected best `size(start_generation)/N` objects + +* `local_optimization_step` (`str`) -- when should we do local optimization? Available values: + + * `'never'` -- don't do local optimization + * `'before_select'` -- before selection best N objects (example: do local optimization for 5N objects and select N best results) + * `'after_select'` -- do local optimization on best selected N objects + +* `local_optimizer` (function) -- local optimization function like: + ```python + def loc_opt(object_as_array, current_score): + # some code + return better_object_as_array, better_score + ``` + +### Select best N of kN + +This little option can help u especially with multimodal tasks. + +![](tests/output/init_best_of.png) + +### Do local optimization + +We can apply some local optimization on start generation before starting GA search. It can be some gradient descent or hill climbing and so on. Also we can apply it before selection best objects (on entire population) or after (on best part of population) and so forth. + +In next example I'm using my [DiscreteHillClimbing](https://github.com/PasaOpasen/DiscreteHillClimbing) algorithm for local optimization my discrete task: + +```python +import numpy as np +import matplotlib.pyplot as plt + +from DiscreteHillClimbing import Hill_Climbing_descent + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Population_initializer + + +def f(arr): + arr2 = arr/25 + return -np.sum(arr2*np.sin(np.sqrt(np.abs(arr2))))**5 + np.sum(np.abs(arr2))**2 + +iterations = 100 + +varbound = [[-100, 100]]*15 + +available_values = [np.arange(-100, 101)]*15 + + +my_local_optimizer = lambda arr, score: Hill_Climbing_descent(function = f, available_predictors_values=available_values, max_function_evals=50, start_solution=arr ) + + +model = ga(function=f, dimension=varbound.shape[0], + variable_type='int', + variable_boundaries = varbound, + algorithm_parameters={ + 'max_num_iteration': iterations, + 'population_size': 400 + }) + + +for time in ('before_select', 'after_select', 'never'): + model.run(no_plot = True, + population_initializer = Population_initializer( + select_best_of = 3, + local_optimization_step = time, + local_optimizer = my_local_optimizer + ) + ) + + plt.plot(model.report, label = f"local optimization time = '{time}'") + + +plt.xlabel('Generation') +plt.ylabel('Minimized function (40 simulations average)') +plt.title('Selection best N object before running GA') +plt.legend() +``` + +![](tests/output/init_local_opt.png) + +### Optimization with oppositions + +Also u can create start population with [oppositions](https://github.com/PasaOpasen/opp-op-pop-init). See [example of code](tests/best_of_N_with_opp.py) + +![](tests/output/init_best_of_opp.png) + +## Revolutions + +U can create [revolutions in your population](https://github.com/PasaOpasen/opp-op-pop-init) after some stagnation steps. It really can help u for some tasks. See [example](tests/revolution.py) + +![](tests/output/revolution.png) + + +## Duplicates removing + +If u remove duplicates each `k` generations, u can speed up the optimization process ([example](tests/remove_dups.py)) + +![](tests/output/remove_dups.png) + +## Cache + +It can be useful for run-speed to use cache with *some discrete tasks*. For this u can import `np_lru_cache` decorator and use it like here: + +```python +import np_lru_cache + +@np_lru_cache(maxsize = some_size) +def minimized_func(arr): + # code + return result + +# +# run +# algorithm +# + + +# don't forget to clear cache +minimized_func.cache_clear() +``` +## Report checker + +Basically the model checks best population score (minimal score of generation) each generation and saves it to `report` field. Actually this sequence of numbers u see in big part of plots. This behavior is needed for several parts and u cannot disable it. But if u want to report some other metric without using [callbacks](#middle-callbacks), there is highly simple and fast way. + +After creating `model` but before running `run()` u need to append ur logic to `model.checked_reports` field. Take a look at example: + +```python +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import plot_several_lines + +def f(X): + return 50*np.sum(X) - np.sum(np.sqrt(X) * np.sin(X)) + +dim = 25 +varbound = [[0 ,10]]*dim + +model = ga(function=f, dimension=dim, + variable_type='real', variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': 600 + } +) + +# here model exists and has checked_reports field +# now u can append any functions to report + +model.checked_reports.extend( + [ + ('report_average', np.mean), + ('report_25', lambda arr: np.quantile(arr, 0.25)), + ('report_50', np.median) + ] +) + +# run optimization process +model.run(no_plot = False) + +# now u have not only model.report but model.report_25 and so on + +#plot reports +names = [name for name, _ in model.checked_reports[::-1]] +plot_several_lines( + lines=[getattr(model, name) for name in names], + colors=('green', 'black', 'red', 'blue'), + labels=['median value', '25% quantile', 'mean of population', 'best pop score'], + linewidths=(1, 1.5, 1, 2), + title="Several custom reports with base reports", + save_as='./output/report.png' +) +``` + +![](tests/output/report.png) + +As u see, u should append tuple `(name of report, func to evaluate report)` to `model.checked_report`. It's highly recommended to start this name with `report_` (e. g. `report_my_median`). And the function u use will get 1D-numpy *sorted* array of population scores. + + +## Middle callbacks + +There is an amazing way to control optimization process using `MiddleCallbacks` class. Just learn next logic: + +1. u can use several `MiddleCallbacks` callbacks as list at `middle_callbacks` parameter in `run()` method +2. each middle callback is the pair of `action` and `condition` functions +3. `condition(data)` (`Callable[[MiddleCallbackData], bool]`) function gets `data` object (dataclass `MiddleCallbackData` from version 6.5.0) about primary model parameters and makes logical decision about applying `action` function +4. `action(data)` (`Callable[[MiddleCallbackData],MiddleCallbackData]`) function modifies `data` objects as u need -- and model will be modified by new `data` +5. `data` object is the structure with several parameters u can modify: + ```python + data = MiddleCallbackData( + last_generation=Generation.from_pop_matrix(pop), + current_generation=t, + report_list=self.report, + + mutation_prob=self.prob_mut, + crossover_prob=self.prob_cross, + mutation=self.real_mutation, + crossover=self.crossover, + selection=self.selection, + + current_stagnation=counter, + max_stagnation=self.max_stagnations, + + parents_portion=self.param.parents_portion, + elit_ratio=self.param.elit_ratio, + + set_function=self.set_function + ) + ``` + So, the `action` function gets `data` objects and returns `data` object. + +It's very simple to create your own `action` and `condition` functions. But there are several popular functions contained in `Actions` and `ActionConditions` classes: +* `actions`: + * `Stop()` -- just stop optimization process + * `ReduceMutationProb(reduce_coef = 0.9)` -- reduce mutation probability + * `ChangeRandomCrossover(available_crossovers: Sequence[Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]])` -- change another (random) crossover from list of crossovers + * `ChangeRandomSelection(available_selections: Sequence[Callable[[np.ndarray, int], np.ndarray]])` + * `ChangeRandomMutation(available_mutations: Sequence[Callable[[float, float, float], float]])` + * `RemoveDuplicates(oppositor = None, creator = None, converter = None)`; see [doc](geneticalgorithm2/callbacks.py) + * `CopyBest(by_indexes)` -- copies best population object values (from dimensions in `by_indexes`) to all population + * `PlotPopulationScores(title_pattern = lambda data: f"Generation {data['current_generation']}", save_as_name_pattern = None)` -- plot population scores; needs 2 functions like `data`->string for title and file name (to save) +* `conditions`: + * `ActionConditions.EachGen(generation_step = 10)` -- do action each `generation_step` generations + * `ActionConditions.Always()` do action each generations, equals to `ActionConditions.EachGen(1)` + * `ActionConditions.AfterStagnation(stagnation_generations = 50)` -- do action after `stagnation_generations` stagnation generations + * `ActionConditions.Several(list_of_conditions)` -- do action if all conditions in list are true + +To combine `action` and `condition` to callback, just use `MiddleCallbacks.UniversalCallback(action, condition)` methods. + + +There are also next high-level useful callbacks: + +* `MiddleCallbacks.ReduceMutationGen(reduce_coef = 0.9, min_mutation = 0.005, reduce_each_generation = 50, reload_each_generation = 500)` +* `MiddleCallbacks.GeneDiversityStats(step_generations_for_plotting:int = 10)` -- plots some duplicates statistics each gen ([example](/tests/plot_diversities.py)) +![](diversity.gif) + + +See [code example](tests/small_middle_callbacks.py) + +## How to compare efficiency of several versions of GA optimization + +To compare efficiency of several versions of GA optimization (such as several values of several hyperparameters or including/excepting some actions like oppositions) u should make some count of simulations and compare results using some statistical test. I have realized this logic [here](https://github.com/PasaOpasen/ab-testing-results-difference) + +## Hints on how to adjust genetic algorithm's parameters (from `geneticalgorithm` package) + +In general the performance of a genetic algorithm or any evolutionary algorithm +depends on its parameters. Parameter setting of an evolutionary algorithm is important. Usually these parameters are adjusted based on experience and by conducting a sensitivity analysis. +It is impossible to provide a general guideline to parameter setting but the suggestions provided below may help: + +* **Number of iterations**: Select a `max_num_iterations` sufficiently large; otherwise the reported solution may not be satisfactory. On the other hand +selecting a very large number of iterations increases the run time significantly. So this is actually a compromise between +the accuracy you want and the time and computational cost you spend. + +* **Population size**: Given a constant number of functional evaluations (`max_num_iterations` times population_size) I would select smaller population size and greater iterations. However, a very small choice of population size is also deteriorative. For most problems I would select a population size of 100 unless the dimension of the problem is very large that needs a bigger population size. + +* **elit_ratio**: Although having few elites is usually a good idea and may increase the rate of convergence in some problems, having too many elites in the population may cause the algorithm to easily trap in a local optima. I would usually select only one elite in most cases. Elitism is not always necessary and in some problems may even be deteriorative. + +* **mutation_probability**: This is a parameter you may need to adjust more than the other ones. Its appropriate value heavily depends on the problem. Sometimes we may select +mutation_probability as small as 0.01 (i.e. 1 percent) and sometimes even as large as 0.5 (i.e. 50 percent) or even larger. In general if the genetic algorithm trapped +in a local optimum increasing the mutation probability may help. On the other hand if the algorithm suffers from stagnation reducing the mutation probability may be effective. However, this rule of thumb is not always true. + +* **parents_portion**: If parents_portion set zero, it means that the whole of the population is filled with the newly generated solutions. +On the other hand having this parameter equals 1 (i.e. 100 percent) means no new solution +is generated and the algorithm would just repeat the previous values without any change which is not meaningful and effective obviously. Anything between these two may work. The exact value depends on the problem. + +* **crossover_type**: Depends on the problem. I would usually use uniform crossover. But testing the other ones in your problem is recommended. + +* **max_iteration_without_improv**: This is a parameter that I recommend being used cautiously. +If this parameter is too small then the algorithm may stop while it trapped in a local optimum. +So make sure you select a sufficiently large criteria to provide enough time for the algorithm to progress and to avoid immature convergence. + +Finally to make sure that the parameter setting is fine, we usually should run the +algorithm for several times and if convergence curves of all runs converged to the same objective function value we may accept that solution as the optimum. The number of runs +depends but usually five or ten runs is prevalent. Notice that in some problems +several possible set of variables produces the same objective function value. +When we study the convergence of a genetic algorithm we compare the objective function values not the decision variables. + +## How to get maximum speed + +### Don't use plotting + +```python +result = model.run( + no_plot = True, +) +``` + +### Don't use progress bar + +```python +result = model.run( + progress_bar_stream = None, +) +``` + +### Try to use faster optimizing function + +Try to speed up your optimizing `function` using Numpy, [Numba](https://numba.pydata.org/) or [Cython](https://cython.org/). If u can, write your own `set_function` (function which applies to whole population samples matrix) with cython optimizations, parallelism and so. + +### Specify custom optimized `mutation`, `crossover`, `selection` + +Write faster implementations for model methods `mut`, `mut_middle`, `crossover`, `selection` and set them before running optimization process: + +```python +model.mut = custom_mut +model.crossover = custom_crossover + +model.run(...) +``` + +### Specify `fill_children` method + +From version `6.8.4` there is `fill_children` model method: + +```python +self.fill_children: Optional[Callable[[array2D, int], None]] = None +``` + +It is empty and does nothing; but if u specify it, u can get huge speed up at very intensive algorithm part. Take a look at [main algo structure](#main-algorithm-structure). There is a part with creating children from parents, this part is the most intensive because it uses python loops, calls sampling, crossover and mutations at each iteration. Using `fill_children`, u can rewrite this logic in your manner to speed up. + +Suppose u have new population matrix `pop` (type `np.float64`, shape `(population_size, dim_count)`) where first `parents_count` rows are selected parents, next rows are filled by random, so inside `fill_children` method u should fill last `population_size - parents_count` rows (children) by using some your logic. Expected (but not mandatory) logic like this: + +```python +for k in range(self.parents_count, self.population_size, 2): + + r1, r2 = get_parents_inds() # get 2 random parents indexes from [0, parents_count) + + pvar1 = pop[r1] + pvar2 = pop[r2] + + ch1, ch2 = self.crossover(pvar1, pvar2) # crossover + + # mutations + ch1 = self.mut(ch1) + ch2 = self.mut_middle(ch2, pvar1, pvar2) + + # put to population + pop[k] = ch1 + pop[k+1] = ch2 +``` + +**Example**. In one task I use this algorithm many times (100 000 generations total), so the speed matters. Every sample item is the index of element in other array there, so `i`th sample element is always integer value from cut `[0, end[i]]`. I use uniform crossover and uniform mutation (work perfect for this task). So I specified creating children logic for this task using cython. + +Content of file `fill_children.pyx`: + +```cython +#!python +#cython: language_level=3 + +import numpy as np + +cimport numpy as np + +np.import_array() + +cimport cython + +import math +import random + +@cython.boundscheck(False) +@cython.wraparound(False) +def fill_children( + np.ndarray[np.float64_t, ndim=2] pop, # samples are integers but always float64 type + int parents_count, # count of already done parents + + float mut_prob, # mutation probability + np.ndarray[np.uint8_t, ndim=1] ends # max elements for each dimension (min elements are 0) +): + + cdef: + Py_ssize_t i, k, population_size = pop.shape[0], dim_count = pop.shape[1], r1, r2 + + float v1, v2, tmp + np.ndarray[np.float64_t, ndim=1] cross, mut, mut_middle + + # making 2 children at each iteration + for k in range(parents_count, population_size, 2): # C loop, not Python + + # + # 2 random parents (fast implementation) + # + + r1 = random.randrange(parents_count) + r2 = random.randrange(parents_count) + if r1 == r2: + while r1 == r2: # C loop! + r2 = random.randrange(parents_count) + + # + # I always need these 3 random probs sequences, so the fastest way to obtain them is np.random.random + # + cross = np.random.random(dim_count) # crossover probabilities for each dimension + mut = np.random.random(dim_count) + mut_middle = np.random.random(dim_count) + + for i in range(dim_count): # C loop for each dimension + v1 = pop[r1, i] # first parent value + v2 = pop[r2, i] # second parent value + + if cross[i] < 0.5: # random swap (uniform crossover), copy otherwise + tmp = v2 + v2 = v1 + v1 = tmp + + if mut[i] < mut_prob: # random mutation for first child + # fastest way to get random integer from [0, ends[i]] + # random.random() calls not always but only on mut[i] < mut_prob + v1 = math.floor(random.random() * (ends[i] + 1)) + + if mut_middle[i] < mut_prob: # mut_middle for second + tmp = random.random() + if v1 < v2: + v2 = v1 + math.floor(tmp * (v2 - v1 + 1)) # integer from [v1, v2], v1 < v2 + elif v1 > v2: + v2 = v2 + math.floor(tmp * (v1 - v2 + 1)) # integer from [v2, v1], v2 < v1 + else: + v2 = math.floor(tmp * (ends[i] + 1)) + + # + # put values to children in array + # + pop[k, i] = v1 + pop[k + 1, i] = v2 + +``` + +After compilation this file I can call it from python file to use inside GA: + +```python + +mut_prob = param['mutation_probability'] + +def fill_children(pop: array2D, parents_count: int): + """wrapper on fill_children.fill_children with putting local variables mut_prob, ends""" + return fill_children.fill_children( + pop, parents_count, mut_prob, ends + ) + +model.fill_children = fill_children + +model.run(...) +``` + + +# Examples pretty collection + +## Optimization test functions + +Here there is the implementation of `geneticalgorithm2` for some benchmark problems. Test functions are got from my [`OptimizationTestFunctions`](https://github.com/PasaOpasen/OptimizationTestFunctions) package. + +The code for optimizations process is same for each function and is contained [in file](tests/optimization_test_functions.py). + +### [Sphere](https://github.com/PasaOpasen/OptimizationTestFunctions#sphere) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Sphere.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Sphere.png) + +### [Ackley](https://github.com/PasaOpasen/OptimizationTestFunctions#ackley) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Ackley.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Ackley.png) + +### [AckleyTest](https://github.com/PasaOpasen/OptimizationTestFunctions#ackleytest) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20AckleyTest.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20AckleyTest.png) + +### [Rosenbrock](https://github.com/PasaOpasen/OptimizationTestFunctions#rosenbrock) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Rosenbrock.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Rosenbrock.png) + +### [Fletcher](https://github.com/PasaOpasen/OptimizationTestFunctions#fletcher) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Fletcher.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Fletcher.png) + +### [Griewank](https://github.com/PasaOpasen/OptimizationTestFunctions#griewank) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Griewank.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Griewank.png) + +### [Penalty2](https://github.com/PasaOpasen/OptimizationTestFunctions#penalty2) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Penalty2.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Penalty2.png) + +### [Quartic](https://github.com/PasaOpasen/OptimizationTestFunctions#quartic) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Quartic.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Quartic.png) + +### [Rastrigin](https://github.com/PasaOpasen/OptimizationTestFunctions#rastrigin) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Rastrigin.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Rastrigin.png) + +### [SchwefelDouble](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefeldouble) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelDouble.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelDouble.png) + +### [SchwefelMax](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelmax) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelMax.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelMax.png) + +### [SchwefelAbs](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelabs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelAbs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelAbs.png) + +### [SchwefelSin](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelsin) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelSin.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelSin.png) + +### [Stairs](https://github.com/PasaOpasen/OptimizationTestFunctions#stairs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Stairs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Stairs.png) + +### [Abs](https://github.com/PasaOpasen/OptimizationTestFunctions#abs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Abs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Abs.png) + +### [Michalewicz](https://github.com/PasaOpasen/OptimizationTestFunctions#michalewicz) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Michalewicz.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Michalewicz.png) + +### [Scheffer](https://github.com/PasaOpasen/OptimizationTestFunctions#scheffer) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Scheffer.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Scheffer.png) + +### [Eggholder](https://github.com/PasaOpasen/OptimizationTestFunctions#eggholder) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Eggholder.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Eggholder.png) + +### [Weierstrass](https://github.com/PasaOpasen/OptimizationTestFunctions#weierstrass) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Weierstrass.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Weierstrass.png) + + + +## Using GA in reinforcement learning + +See [example of using GA optimization with keras neural networks](https://www.kaggle.com/demetrypascal/opengym-tasks-using-keras-and-geneticalgorithm2) for solving OpenGym tasks. + +Better example is [OpenGym using cost2fitness and geneticalgorithm2](https://www.kaggle.com/demetrypascal/opengym-using-cost2fitness-and-geneticalgorithm2) where I use also my [cost2fitness](https://github.com/PasaOpasen/cost2fitness) package for fast forward propagation + + +## Using GA with image reconstruction by polygons + +Links: +1. https://www.kaggle.com/demetrypascal/fork-of-imagereconstruction-with-geneticalgorithm2 +2. https://www.kaggle.com/demetrypascal/imagereconstructionpolygons-with-geneticalgorithm2 + + +# Popular questions + +## How to disable autoplot? + +Just use `no_plot = True` param in `run` method: + +```python +model.run(no_plot = True) +``` + +If u want, u can plot results later by using + +```python +model.plot_results() +``` + +Also u can create your pretty plots using `model.report` object (it's a list of values): + +```python +re = np.array(model.report) + +plt.plot(re) +plt.xlabel('Iteration') +plt.ylabel('Objective function') +plt.title('Genetic Algorithm') +plt.show() +``` + +## How to plot population scores? + +There are 2 ways to plot of scores of population: +* use `plot_pop_scores(scores, title = 'Population scores', save_as = None)` function from `geneticalgorithm2` environment +* use `plot_generation_scores(self, title = 'Last generation scores', save_as = None)` method of `ga` object for plotting scores of last generation (yes, it's wrapper of previous function) + +Let's check example: +```python +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga + +from geneticalgorithm2 import plot_pop_scores # for plotting scores without ga object + +def f(X): + return 50*np.sum(X) - np.sum(np.sqrt(X)*np.sin(X)) + +dim = 25 +varbound = [[0,10]]*dim + +# create start population +start_pop = np.random.uniform(0, 10, (50, dim)) +# eval scores of start population +start_scores = np.array([f(start_pop[i]) for i in range(start_pop.shape[0])]) + +# plot start scores using plot_pop_scores function +plot_pop_scores(start_scores, title = 'Population scores before beginning of searching', save_as= 'plot_scores_start.png') + + +model = ga(function=f, dimension=dim, variable_type='real', variable_boundaries=varbound) +# run optimization process +model.run(no_plot = True, + start_generation={ + 'variables': start_pop, + 'scores': start_scores + }) +# plot and save optimization process plot +model.plot_results(save_as = 'plot_scores_process.png') + +# plot scores of last population +model.plot_generation_scores(title = 'Population scores after ending of searching', save_as= 'plot_scores_end.png') +``` +![](tests/output/plot_scores_start.png) +![](tests/output/plot_scores_process.png) +![](tests/output/plot_scores_end.png) + + + +## How to specify evaluated function for all population? + +U can do it using `set_function` parameter into `run()` method. + +This function should get `numpy 2D-array` (samples x dimension) and return `1D-array` with results. + +By default it uses `set_function = geneticalgorithm2.default_set_function(function)`, where + +```python + def default_set_function(function_for_set): + def func(matrix): + return np.array([function_for_set(matrix[i,:]) for i in range(matrix.shape[0])]) + return func +``` +U may want to use it for creating some specific or fast-vectorized evaluations like here: + +```python + +def sigmoid(z): + return 1/(1+np.exp(-z)) + +matrix = np.random.random((1000,100)) + +def vectorised(X): + return sigmoid(matrix.dot(X)) + +model.run(set_function = vectorised) +``` + +## What about parallelism? + +By using `set_function` u can determine your own behavior for parallelism or u can use `geneticalgorithm2.set_function_multiprocess(f, n_jobs = -1)` for using just parallelism (recommended for heavy functions and big populations, not recommended for fast functions and small populations). + +For example: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + import math + a = X[0] + b = X[1] + c = X[2] + s = 0 + for i in range(10000): + s += math.sin(a*i) + math.sin(b*i) + math.cos(c*i) + + return s + + +algorithm_param = {'max_num_iteration': 50, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None} + +varbound = np.array([[-10,10]]*3) + +model = ga(function=f, dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = algorithm_param) + +######## + +%time model.run() +# Wall time: 1min 52s + +%time model.run(set_function= ga.set_function_multiprocess(f, n_jobs = 6)) +# Wall time: 31.7 s +``` + +## How to initialize start population? How to continue optimization with new run? + +For this there is `start_generation` parameter in `run()` method. It's the dictionary with structure like returned `model.output_dict['last_generation']`. Let's see example how can u to use it: + +```python +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +dim = 6 + +varbound = [(0,10)]*dim + + +algorithm_param = {'max_num_iteration': 500, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'max_iteration_without_improv':None} + +model = ga(function=f, + dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = algorithm_param) + +# start generation +# as u see u can use any values been valid for ur function +samples = np.random.uniform(0, 50, (300, dim)) # 300 is the new size of your generation + + + +model.run(no_plot = False, start_generation={'variables':samples, 'scores': None}) +# it's not necessary to evaluate scores before +# but u can do it if u have evaluated scores and don't wanna repeat calculations + + + +# from version 6.3.0 it's recommended to use this form +from geneticalgorithm2 import Generation +model.run(no_plot = False, start_generation=Generation(variables = samples, scores = None)) + + +# from version 6.4.0 u also can use these forms +model.run(no_plot = False, start_generation= samples) +model.run(no_plot = False, start_generation= (samples, None)) + + +# if u have scores array, u can put it too +scores = np.array([f(sample) for sample in samples]) +model.run(no_plot = False, start_generation= (samples, scores)) + + +## +## after first run +## best value = 0.10426190111045064 +## + +# okay, let's continue optimization using saved last generation +model.run(no_plot = True, start_generation=model.output_dict['last_generation']) + +## +## after second run +## best value = 0.06128462776296528 +## + +``` + +Also u can save and load populations using likely code: + +```python +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga + +from OptimizationTestFunctions import Eggholder + + +dim = 2*15 + +f = Eggholder(dim) + +xmin, xmax, ymin, ymax = f.bounds + +varbound = np.array([[xmin, xmax], [ymin, ymax]]*15) + +model = ga(function=f, + dimension = dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = { + 'max_num_iteration': 300, + 'population_size': 100 + }) + +# first run and save last generation to file +filename = "eggholder_lastgen.npz" +model.run(save_last_generation_as = filename) + + +# load start generation from file and run again (continue optimization) +model.run(start_generation=filename) +``` + + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8d2101f --- /dev/null +++ b/README.rst @@ -0,0 +1,56 @@ +geneticalgorithm2 +================= + + +geneticalgorithm2 is a Python library distributed on +`Pypi `__ for implementing standard and elitist +`genetic-algorithm `__ +(GA). This package solves continuous, +`combinatorial `__ +and mixed +`optimization `__ +problems with continuous, discrete, and mixed variables. It provides an +easy implementation of genetic-algorithm (GA) in Python. + +Installation / PLEASE CHECK THE HOMEPAGE FOR CURRENT EXAMPLES OF USING +---------------------------------------------------------------------- + +Use the package manager `pip `__ to +install geneticalgorithm2 in Python. + +.. code:: python + + pip install geneticalgorithm2 + +A simple example +---------------- + +Assume we want to find a set of X=(x1,x2,x3) that minimizes function f(X)=x1+x2+x3 where X can be any real number in [0,10]. +This is a trivial problem and we already know that the answer is X=(0,0,0) where f(X)=0. We just use this simple example to see how to implement geneticalgorithm: + +First we import geneticalgorithm and `numpy `__. +Next, we define function f which we want to minimize and the boundaries +of the decision variables; Then simply geneticalgorithm is called to +solve the defined optimization problem as follows: + +.. code:: python + + import numpy as np + from geneticalgorithm2 import geneticalgorithm2 as ga + + def f(X): + return np.sum(X) + + + varbound=np.array([[0,10]]*3) + + model=ga(function=f,dimension=3,variable_type='real',variable_boundaries=varbound) + + model.run() + +Notice that we define the function f so that its output is the objective +function we want to minimize where the input is the set of X (decision +variables). The boundaries for variables must be defined as a numpy +array and for each variable we need a separate boundary. Here I have +three variables and all of them have the same boundaries (For the case +the boundaries are different see the example with mixed variables). diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..fc24e7a --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-hacker \ No newline at end of file diff --git a/diversity.gif b/diversity.gif new file mode 100644 index 0000000..40be19b Binary files /dev/null and b/diversity.gif differ diff --git a/genetic_algorithm_convergence.gif b/genetic_algorithm_convergence.gif new file mode 100644 index 0000000..07b3449 Binary files /dev/null and b/genetic_algorithm_convergence.gif differ diff --git a/geneticalgorithm2/__init__.py b/geneticalgorithm2/__init__.py new file mode 100644 index 0000000..95d0b1a --- /dev/null +++ b/geneticalgorithm2/__init__.py @@ -0,0 +1,43 @@ +''' + +Copyright 2020 Ryan (Mohammad) Solgi, Demetry Pascal + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +''' + + +from .classes import Generation, AlgorithmParams + +from .geneticalgorithm2 import geneticalgorithm2 + +from .mutations import Mutations +from .crossovers import Crossover +from .selections import Selection + +from .initializer import Population_initializer + +from .cache import np_lru_cache +from .callbacks import Callbacks, Actions, ActionConditions, MiddleCallbacks + +from .plotting_tools import plot_pop_scores, plot_several_lines + + + + diff --git a/geneticalgorithm2/aliases.py b/geneticalgorithm2/aliases.py new file mode 100644 index 0000000..e474ffc --- /dev/null +++ b/geneticalgorithm2/aliases.py @@ -0,0 +1,17 @@ + + +from typing import List, Tuple, Dict, Sequence, Optional, Any, Callable, Union, TypeVar, Literal +from typing_extensions import TypeAlias + +import os + + +Number: TypeAlias = Union[int, float] + +import numpy as np + +array1D: TypeAlias = np.ndarray +array2D: TypeAlias = np.ndarray + +PathLike: TypeAlias = Union[str, os.PathLike] + diff --git a/geneticalgorithm2/cache.py b/geneticalgorithm2/cache.py new file mode 100644 index 0000000..7648cf6 --- /dev/null +++ b/geneticalgorithm2/cache.py @@ -0,0 +1,60 @@ + +from typing import Callable + +import numpy as np + +from functools import lru_cache, wraps + +#from fastcache import clru_cache + +from .aliases import array1D + + +def np_lru_cache(*args, **kwargs): + """ + LRU cache implementation for functions whose FIRST parameter is a numpy array + forked from: https://gist.github.com/Susensio/61f4fee01150caaac1e10fc5f005eb75 + """ + + def decorator(function: Callable): + + @wraps(function) + def wrapper(np_array: array1D): + return cached_wrapper(tuple(np_array)) + + @lru_cache(*args, **kwargs) + #@clru_cache(*args, **kwargs) + def cached_wrapper(hashable_array): + return function(np.array(hashable_array)) + + # copy lru_cache attributes over too + wrapper.cache_info = cached_wrapper.cache_info + wrapper.cache_clear = cached_wrapper.cache_clear + return wrapper + + return decorator + + + + + +if __name__ == '__main__': + + ar = np.random.randint(0, 100, (1000, 100)) + + f = lambda arr: np.std(arr + arr/(1 + arr**2) - arr + np.sin(arr) * np.cos(arr) + 2) + + def no_c(arr): + return f(arr) + + @np_lru_cache(maxsize=700, typed=True) + def with_c(arr): + return f(arr) + + #%time for _ in range(50): [no_c(arr) for arr in ar[np.random.rand(ar.shape[0]).argsort()]] + #%time for _ in range(50): [with_c(arr) for arr in ar[np.random.rand(ar.shape[0]).argsort()]] + + + + + diff --git a/geneticalgorithm2/callbacks.py b/geneticalgorithm2/callbacks.py new file mode 100644 index 0000000..c418d12 --- /dev/null +++ b/geneticalgorithm2/callbacks.py @@ -0,0 +1,459 @@ + +from typing import List, Optional, Callable, Tuple, Sequence + +import os +import random + +import numpy as np + +from OppOpPopInit import OppositionOperators, SampleInitializers + +from .aliases import TypeAlias, array1D, array2D, PathLike +from .files import mkdir_of_file, mkdir + +from .classes import MiddleCallbackData, Generation + +from .utils import union_to_matrix, fast_max + +from .crossovers import CrossoverFunc +from .selections import SelectionFunc +from .mutations import MutationFunc + + +CallbackFunc: TypeAlias = Callable[[int, List[float], array2D, array1D], None] +MiddleCallbackActionFunc: TypeAlias = Callable[[MiddleCallbackData], MiddleCallbackData] +MiddleCallbackConditionFunc: TypeAlias = Callable[[MiddleCallbackData], bool] +MiddleCallbackFunc: TypeAlias = Callable[[MiddleCallbackData], Tuple[MiddleCallbackData, bool]] + + +class Callbacks: + + @staticmethod + def NoneCallback(): + return lambda generation_number, report_list, last_population, last_scores: None + + @staticmethod + def SavePopulation(folder: PathLike, save_gen_step: int = 50, file_prefix: str = 'population') -> CallbackFunc: + + mkdir(folder) + + def func(generation_number: int, report_list: List[float], last_population: array2D, last_scores: array1D): + + if generation_number % save_gen_step != 0: + return + + Generation(last_population, last_scores).save( + os.path.join( + folder, + f"{file_prefix}_{generation_number}.npz" + ) + ) + + return func + + @staticmethod + def PlotOptimizationProcess( + folder: PathLike, + save_gen_step: int = 50, + show: bool = False, + main_color: str = 'green', + file_prefix: str = 'report' + ) -> CallbackFunc: + import matplotlib.pyplot as plt + from matplotlib.ticker import MaxNLocator + + mkdir(folder) + + def func(generation_number: int, report_list: List[float], last_population: array2D, last_scores: array1D): + + if generation_number % save_gen_step != 0: + return + + # if len(report_list) == 0: + # sys.stdout.write("No results to plot!\n") + # return + + ax = plt.axes() + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + + plt.plot( + np.arange(1, 1 + len(report_list)), + report_list, + color=main_color, + label='best of generation', + linewidth=2 + ) + + plt.xlabel('Generation') + plt.ylabel('Minimized function') + plt.title('GA optimization process') + plt.legend() + + plt.savefig(os.path.join(folder, f"{file_prefix}_{generation_number}.png"), dpi=200) + + if show: + plt.show() + else: + plt.close() + + return func + + +class Actions: + + @staticmethod + def Stop(reason_name: str = 'stopped by Stop callback') -> MiddleCallbackActionFunc: + + def func(data: MiddleCallbackData): + data.reason_to_stop = reason_name + return data + return func + + @staticmethod + def ReduceMutationProb(reduce_coef: float = 0.9) -> MiddleCallbackActionFunc: + + def func(data: MiddleCallbackData): + data.mutation_prob *= reduce_coef + return data + + return func + + + #def DualStrategyStep(): + # pass + + #def SetFunction(): + # pass + + + @staticmethod + def ChangeRandomCrossover( + available_crossovers: Sequence[CrossoverFunc] + ) -> MiddleCallbackActionFunc: + + def func(data: MiddleCallbackData): + data.crossover = random.choice(available_crossovers) + return data + + return func + + @staticmethod + def ChangeRandomSelection(available_selections: Sequence[SelectionFunc]) -> MiddleCallbackActionFunc: + + def func(data: MiddleCallbackData): + data.selection = random.choice(available_selections) + return data + + return func + + @staticmethod + def ChangeRandomMutation(available_mutations: Sequence[MutationFunc]) -> MiddleCallbackActionFunc: + + def func(data): + data.mutation = random.choice(available_mutations) + return data + + return func + + + @staticmethod + def RemoveDuplicates( + oppositor: Optional[Callable[[array1D], array1D]] = None, + creator: Optional[Callable[[], array1D]] = None, + converter: Optional[Callable[[array1D], array1D]] = None + ) -> MiddleCallbackActionFunc: + """ + Removes duplicates from population + + Parameters + ---------- + oppositor : oppositor from OppOpPopInit, optional + oppositor for applying after duplicates removing. By default -- using just random initializer from creator. + The default is None. + creator : the function creates population samples, optional + the function creates population samples if oppositor is None. The default is None. + converter : func, optional + function converts population samples in new format to compare (if needed). The default is None. + + """ + + if creator is None and oppositor is None: + raise Exception("No functions to fill population! creator or oppositors must be not None") + + if converter is None: + def without_dup(pop: array2D, scores: array1D) -> Tuple[array2D, int]: + """returns population without dups""" + _, index_of_dups = np.unique(pop, axis=0, return_index=True) + return union_to_matrix(pop[index_of_dups], scores[index_of_dups]), pop.shape[0] - index_of_dups.size + else: + def without_dup(pop: array2D, scores: array1D) -> Tuple[array2D, int]: + """returns population without dups""" + _, index_of_dups = np.unique( + np.array([converter(pop[i]) for i in range(pop.shape[0])]), axis=0, return_index=True + ) + return union_to_matrix(pop[index_of_dups], scores[index_of_dups]), pop.shape[0] - index_of_dups.size + + + if oppositor is None: + def remover(pop: array2D, scores: array1D, set_function: Callable[[array2D], array1D]) -> array2D: + + pp, count_to_create = without_dup(pop, scores) # pop without dups + pp2 = np.empty((count_to_create, pp.shape[1])) + pp2[:, :-1] = SampleInitializers.CreateSamples(creator, count_to_create) # new pop elements + pp2[:, -1] = set_function(pp2[:, :-1]) # new elements values + + new_pop = np.vstack((pp, pp2)) + + return new_pop[np.argsort(new_pop[:, -1])] # new pop + + else: # using oppositors + def remover(pop: array2D, scores: array1D, set_function: Callable[[array2D], array1D]) -> array2D: + + pp, count_to_create = without_dup(pop, scores) # pop without dups + + if count_to_create > pp.shape[0]: + raise Exception("Too many duplicates, cannot oppose") + + if count_to_create == 0: + return pp[np.argsort(pp[:, -1])] + + pp2 = np.empty((count_to_create, pp.shape[1])) + # oppose count_to_create worse elements + pp2[:, :-1] = OppositionOperators.Reflect(pp[-count_to_create:, :-1], oppositor) # new pop elements + pp2[:, -1] = set_function(pp2[:, :-1]) # new elements values + + new_pop = np.vstack((pp, pp2)) + + return new_pop[np.argsort(new_pop[:, -1])] # new pop + + def func(data: MiddleCallbackData): + new_pop = remover( + data.last_generation.variables, + data.last_generation.scores, + data.set_function + ) + + data.last_generation = Generation.from_pop_matrix(new_pop) + + return data + + + return func + + @staticmethod + def CopyBest(by_indexes: Sequence[int]) -> MiddleCallbackActionFunc: + """ + Copies best population object values (from dimensions in by_indexes) to all population + """ + + if type(by_indexes) != np.ndarray: + by_indexes = np.array(by_indexes) + + def func(data: MiddleCallbackData): + + pop = data.last_generation.variables + scores = data.last_generation.scores + + pop[:, by_indexes] = pop[np.argmin(scores), by_indexes] + + data.last_generation = Generation(pop, data.set_function(pop)) + + return data + return func + + @staticmethod + def PlotPopulationScores( + title_pattern: Callable[[MiddleCallbackData], str] = lambda data: f"Generation {data.current_generation}", + save_as_name_pattern: Optional[Callable[[MiddleCallbackData], str]] = None + ) -> MiddleCallbackActionFunc: + """ + plots population scores + needs 2 functions like data->str for title and file name + """ + from .plotting_tools import plot_pop_scores + + use_save_as = (lambda data: None) if save_as_name_pattern is None else save_as_name_pattern + + def local_plot_callback(data: MiddleCallbackData): + + plot_pop_scores( + data.last_generation.scores, + title=title_pattern(data), + save_as=use_save_as(data) + ) + + return data + + return local_plot_callback + + +class ActionConditions: + + @staticmethod + def EachGen(generation_step: int = 10) -> MiddleCallbackConditionFunc: + + if generation_step < 1 or type(generation_step) is not int: + raise Exception(f"Invalid generation step {generation_step}! Should be int and >=1") + + if generation_step == 1: + return ActionConditions.Always() + + def func(data: MiddleCallbackData): + return data.current_generation % generation_step == 0 and data.current_generation > 0 + + return func + + @staticmethod + def Always() -> MiddleCallbackConditionFunc: + """ + makes action each generation + """ + return lambda data: True + + @staticmethod + def AfterStagnation(stagnation_generations: int = 50) -> MiddleCallbackConditionFunc: + + def func(data: MiddleCallbackData): + return data.current_stagnation % stagnation_generations == 0 and data.current_stagnation > 0 + return func + + + @staticmethod + def Several(conditions: Sequence[MiddleCallbackConditionFunc]) -> MiddleCallbackConditionFunc: + """ + returns function which checks all conditions from conditions + """ + + def func(data: MiddleCallbackData): + return all(cond(data) for cond in conditions) + + return func + + +class MiddleCallbacks: + + @staticmethod + def UniversalCallback( + action: MiddleCallbackActionFunc, + condition: MiddleCallbackConditionFunc, + set_data_after_callback: bool = True + ) -> MiddleCallbackFunc: + + def func(data: MiddleCallbackData): + + cond = condition(data) + if cond: + data = action(data) + + return data, (cond if set_data_after_callback else False) + + return func + + @staticmethod + def ReduceMutationGen( + reduce_coef: float = 0.9, + min_mutation: float = 0.005, + reduce_each_generation: int = 50, + reload_each_generation: int = 500 + ) -> MiddleCallbackFunc: + + start_mutation = None + + def func(data: MiddleCallbackData): + nonlocal start_mutation + + gen = data.current_generation + mut = data.mutation_prob + + if start_mutation is None: + start_mutation = mut + + c1 = gen % reduce_each_generation == 0 + c2 = gen % reload_each_generation == 0 + + if c2: + mut = start_mutation + elif c1: + mut *= reduce_coef + mut = fast_max(mut, min_mutation) + + data.mutation_prob = mut + + return data, (c1 or c2) + + return func + + #def ReduceMutationStagnation(reduce = 0.5, stagnation_gens = 50): + # pass + + @staticmethod + def GeneDiversityStats(step_generations_for_plotting: int = 10) -> MiddleCallbackFunc: + + if step_generations_for_plotting < 1: + raise Exception(f"Wrong step = {step_generations_for_plotting}, should be int and > 0!!") + + import matplotlib.pyplot as plt + + div = [] + count = [] + most = [] + + + def func(data: MiddleCallbackData): + + nonlocal div, count, most + + dt = data.last_generation.variables + + uniq, counts = np.unique(dt, return_counts=True, axis=0) + # raise Exception() + count.append(counts.size) + most.append(counts.max()) + + gene_diversity = 0 + for index in range(dt.shape[0]-1): + gene_diversity += np.count_nonzero(dt[index, :] != dt[index:, :]) / (dt.shape[0] - index) + div.append(gene_diversity/dt.shape[1]) + + if data.current_generation % step_generations_for_plotting == 0: + + fig, axes = plt.subplots(3, 1) + + (ax1, ax2, ax3) = axes + + ax1.plot(count) + #axs[0, 0].set_title('Axis [0, 0]') + + ax2.plot(most, 'tab:orange') + #axs[0, 1].set_title('Axis [0, 1]') + + ax3.plot(div, 'tab:green') + #axs[1, 0].set_title('Axis [1, 0]') + + ylabs = [ + 'Count of unique objects', + 'Count of most popular object', + 'Simple gene diversity' + ] + + for i, ax in enumerate(axes): + ax.set(xlabel='Generation number') + ax.set_title(ylabs[i]) + + # Hide x labels and tick labels for top plots and y ticks for right plots. + for ax in axes: + ax.label_outer() + + fig.suptitle(f'Diversity report (pop size = {dt.shape[0]})') + fig.tight_layout() + plt.show() + + return data, False + + return func + + + + + + diff --git a/geneticalgorithm2/classes.py b/geneticalgorithm2/classes.py new file mode 100644 index 0000000..17cc9f7 --- /dev/null +++ b/geneticalgorithm2/classes.py @@ -0,0 +1,368 @@ + +from typing import Dict, Any, List, Optional, Union, Callable, Tuple, Literal + + +from dataclasses import dataclass +import warnings + +import numpy as np + +from .aliases import array1D, array2D, TypeAlias, PathLike +from .files import mkdir_of_file + +from .crossovers import Crossover, CrossoverFunc +from .mutations import Mutations, MutationIntFunc, MutationFloatFunc +from .selections import Selection, SelectionFunc + +from .utils import can_be_prob, union_to_matrix + + +class DictLikeGetSet: + def __getitem__(self, item): + return getattr(self, item) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def get(self, item): + return getattr(self, item) + + + +_algorithm_params_slots = { + 'max_num_iteration', + 'max_iteration_without_improv', + 'population_size', + 'mutation_probability', + 'mutation_discrete_probability', + 'elit_ratio', + 'crossover_probability', + 'parents_portion', + 'crossover_type', + 'mutation_type', + 'mutation_discrete_type', + 'selection_type' +} + + +@dataclass +class AlgorithmParams(DictLikeGetSet): + + max_num_iteration: Optional[int] = None + max_iteration_without_improv: Optional[int] = None + + population_size: int = 100 + + mutation_probability: float = 0.1 + mutation_discrete_probability: Optional[float] = None + + # deprecated + crossover_probability: Optional[float] = None + + elit_ratio: float = 0.04 + parents_portion: float = 0.3 + + crossover_type: Union[str, CrossoverFunc] = 'uniform' + mutation_type: Union[str, MutationFloatFunc] = 'uniform_by_center' + mutation_discrete_type: Union[str, MutationIntFunc] = 'uniform_discrete' + selection_type: Union[str, SelectionFunc] = 'roulette' + + __annotations__ = { + 'max_num_iteration': Optional[int], + 'max_iteration_without_improv': Optional[int], + 'population_size': int, + 'mutation_probability': float, + 'mutation_discrete_probability': Optional[float], + 'crossover_probability': Optional[float], + 'elit_ratio': float, + 'parents_portion': float, + 'crossover_type': Union[str, CrossoverFunc], + 'mutation_type': Union[str, MutationFloatFunc], + 'mutation_discrete_type': Union[str, MutationIntFunc], + 'selection_type': Union[str, SelectionFunc] + } + + def check_if_valid(self) -> None: + + assert int(self.population_size) > 0, f"population size must be integer and >0, not {self.population_size}" + assert (can_be_prob(self.parents_portion)), "parents_portion must be in range [0,1]" + assert (can_be_prob(self.mutation_probability)), "mutation_probability must be in range [0,1]" + assert (can_be_prob(self.elit_ratio)), "elit_ratio must be in range [0,1]" + + if self.max_iteration_without_improv is not None and self.max_iteration_without_improv < 1: + warnings.warn( + f"max_iteration_without_improv is {self.max_iteration_without_improv} but must be None or int > 0" + ) + self.max_iteration_without_improv = None + + def get_CMS_funcs(self) -> Tuple[ + CrossoverFunc, + MutationFloatFunc, + MutationIntFunc, + SelectionFunc + ]: + """ + returns gotten crossover, mutation, discrete mutation, selection + as necessary functions + """ + + result: List[Callable] = [] + for name, value, dct in ( + ('crossover', self.crossover_type, Crossover.crossovers_dict()), + ('mutation', self.mutation_type, Mutations.mutations_dict()), + ('mutation_discrete', self.mutation_discrete_type, Mutations.mutations_discrete_dict()), + ('selection', self.selection_type, Selection.selections_dict()) + ): + if isinstance(value, str): + if value not in dct: + raise ValueError( + f"unknown name of {name}: '{value}', must be from {tuple(dct.keys())} or a custom function" + ) + result.append(dct[value]) + else: + assert callable(value), f"{name} must be string or callable" + result.append(value) + + return tuple(result) + + @staticmethod + def from_dict(dct: Dict[str, Any]): + + result = AlgorithmParams() + + for name, value in dct.items(): + if name not in _algorithm_params_slots: + raise AttributeError(f"name '{name}' does not exists in AlgorithmParams fields") + + setattr(result, name, value) + return result + + +GenerationConvertible: TypeAlias = Union[ + 'Generation', + str, + Dict[Literal['population', 'scores'], Union[array2D, array1D]], + array2D, + Tuple[ + Optional[array2D], + Optional[array1D] + ] +] + + +@dataclass +class Generation(DictLikeGetSet): + variables: Optional[array2D] = None + scores: Optional[array1D] = None + + __annotations__ = { + 'variables': Optional[array2D], + 'scores': Optional[array1D] + } + + def __check_dims(self) -> None: + if self.variables is not None: + assert len(self.variables.shape) == 2, ( + f"'variables' must be matrix with shape (objects, dimensions), not {self.variables.shape}" + ) + if self.scores is not None: + assert len(self.scores.shape) == 1, f"'scores' must be 1D-array, not with shape {self.scores.shape}" + assert self.variables.shape[0] == self.scores.size, ( + f"count of objects ({self.variables.shape[0]}) " + f"must be equal to count of scores ({self.scores.size})" + ) + + @property + def size(self) -> int: + return self.scores.size + + @property + def dim_size(self) -> int: + return self.variables.shape[1] + + def as_wide_matrix(self) -> array2D: + # should not be used in main code -- was needed for old versions + return union_to_matrix(self.variables, self.scores) + + def save(self, path: PathLike): + mkdir_of_file(path) + np.savez(path, population=self.variables, scores=self.scores) + + @staticmethod + def load(path: PathLike): + try: + st = np.load(path) + except Exception as err: + raise Exception( + f"if generation object is a string, " + f"it must be path to npz file with needed content, but raised exception {repr(err)}" + ) + + assert 'population' in st and 'scores' in st, ( + "saved generation object must contain 'population' and 'scores' fields" + ) + + return Generation(variables=st['population'], scores=st['scores']) + + @staticmethod + def from_object( + dim: int, + object: GenerationConvertible + ): + + obj_type = type(object) + + if obj_type == str: + + generation = Generation.load(object) + + elif obj_type == np.ndarray: + + assert len(object.shape) == 2 and (object.shape[1] == dim or object.shape[1] == dim + 1), ( + f"if start_generation is numpy array, " + f"it must be with shape (samples, dim) or (samples, dim+1), not {object.shape}" + ) + + generation = Generation(object, None) if object.shape[1] == dim else Generation.from_pop_matrix(object) + + elif obj_type == tuple: + + assert len(object) == 2, ( + f"if start_generation is tuple, " + f"it must be tuple with 2 components, not {len(object)}" + ) + + variables, scores = object + + assert ( (variables is None or scores is None) or (variables.shape[0] == scores.size)), ( + "start_generation object must contain variables and scores components " + "which are None or 2D- and 1D-arrays with same shape" + ) + + generation = Generation(variables=variables, scores=scores) + + elif obj_type == dict: + assert ( + ('variables' in object and 'scores' in object) and + (object['variables'] is None or object['scores'] is None) or + (object['variables'].shape[0] == object['scores'].size) + ), ( + "start_generation object must contain 'variables' and 'scores' keys " + "which are None or 2D- and 1D-arrays with same shape" + ) + + generation = Generation(variables=object['variables'], scores=object['scores']) + + elif obj_type == Generation: + generation = Generation(variables=object['variables'], scores=object['scores']) + else: + raise TypeError( + f"invalid type of generation! " + f"Must be in (Union[str, Dict[str, np.ndarray], Generation, np.ndarray, " + f"Tuple[Optional[np.ndarray], Optional[np.ndarray]]]), " + f"not {obj_type}" + ) + + generation.__check_dims() + + if generation.variables is not None: + assert generation.dim_size == dim, ( + f"generation dimension size {generation.dim_size} does not equal to target size {dim}" + ) + + return generation + + @staticmethod + def from_pop_matrix(pop: array2D): + warnings.warn("depricated! pop matrix style will be removed at version 7, use samples and scores separetly") + return Generation( + variables=pop[:, :-1], + scores=pop[:, -1] + ) + + +@dataclass +class GAResult(DictLikeGetSet): + + last_generation: Generation + + __annotations__ = { + 'last_generation': Generation + } + + @property + def variable(self) -> array1D: + return self.last_generation.variables[0] + + @property + def score(self) -> float: + return self.last_generation.scores[0] + + @property + def function(self): + warnings.warn( + f"'function' field is deprecated, will be removed in version 7, use 'score' to get best population score" + ) + return self.score + + +@dataclass +class MiddleCallbackData(DictLikeGetSet): + """ + data object using with middle callbacks + """ + + reason_to_stop: Optional[str] + + last_generation: Generation + + current_generation: int + report_list: List[float] + + mutation_prob: float + mutation_discrete_prob: float + + mutation: MutationFloatFunc + mutation_discrete: MutationIntFunc + crossover: CrossoverFunc + selection: SelectionFunc + + current_stagnation: int + max_stagnation: int + + parents_portion: float + elit_ratio: float + + set_function: Callable[[array2D], array1D] + + __annotations__ = { + 'reason_to_stop': Optional[str], + 'last_generation': Generation, + 'current_generation': int, + 'report_list': List[float], + 'mutation_prob': float, + 'mutation_discrete_prob': float, + 'mutation': MutationFloatFunc, + 'mutation_discrete': MutationIntFunc, + 'crossover': CrossoverFunc, + 'selection': SelectionFunc, + 'current_stagnation': int, + 'max_stagnation': int, + 'parents_portion': float, + 'elit_ratio': float, + 'set_function': Callable[[array2D], array1D] + } + + + + + + + + + + + + + + diff --git a/geneticalgorithm2/crossovers.py b/geneticalgorithm2/crossovers.py new file mode 100644 index 0000000..57250d9 --- /dev/null +++ b/geneticalgorithm2/crossovers.py @@ -0,0 +1,174 @@ + +from typing import Callable, Tuple, Dict + +import random +import numpy as np + +from .aliases import TypeAlias, array1D + +CrossoverFunc: TypeAlias = Callable[[array1D, array1D], Tuple[array1D, array1D]] + + +def get_copies(x: array1D, y: array1D) -> Tuple[array1D, array1D]: + return x.copy(), y.copy() + + +class Crossover: + + @staticmethod + def crossovers_dict() -> Dict[str, CrossoverFunc]: + return { + 'one_point': Crossover.one_point(), + 'two_point': Crossover.two_point(), + 'uniform': Crossover.uniform(), + 'segment': Crossover.segment(), + 'shuffle': Crossover.shuffle(), + } + + @staticmethod + def one_point() -> CrossoverFunc: + + def func(x: array1D, y: array1D): + ofs1, ofs2 = get_copies(x, y) + + ran = np.random.randint(0, x.size) + + ofs1[:ran] = y[:ran] + ofs2[:ran] = x[:ran] + + return ofs1, ofs2 + return func + + @staticmethod + def two_point() -> CrossoverFunc: + + def func(x: array1D, y: array1D): + ofs1, ofs2 = get_copies(x, y) + + ran1 = np.random.randint(0, x.size) + ran2 = np.random.randint(ran1, x.size) + + ofs1[ran1:ran2] = y[ran1:ran2] + ofs2[ran1:ran2] = x[ran1:ran2] + + return ofs1, ofs2 + return func + + @staticmethod + def uniform() -> CrossoverFunc: + + def func(x: array1D, y: array1D): + ofs1, ofs2 = get_copies(x, y) + + ran = np.random.random(x.size) < 0.5 + ofs1[ran] = y[ran] + ofs2[ran] = x[ran] + + return ofs1, ofs2 + + return func + + @staticmethod + def segment(prob: int = 0.6) -> CrossoverFunc: + + def func(x: array1D, y: array1D): + + ofs1, ofs2 = get_copies(x, y) + + p = np.random.random(x.size) < prob + + for i, val in enumerate(p): + if val: + ofs1[i], ofs2[i] = ofs2[i], ofs1[i] + + return ofs1, ofs2 + + return func + + @staticmethod + def shuffle() -> CrossoverFunc: + + def func(x: array1D, y: array1D): + + ofs1, ofs2 = get_copies(x, y) + + index = np.random.choice(np.arange(0, x.size), x.size, replace = False) + + ran = np.random.randint(0, x.size) + + for i in range(ran): + ind = index[i] + ofs1[ind] = y[ind] + ofs2[ind] = x[ind] + + return ofs1, ofs2 + + return func + + @staticmethod + def uniform_window(window: int = 7) -> CrossoverFunc: + + base_uniform = Crossover.uniform() + + def func(x: np.ndarray, y: np.ndarray): + + if x.size % window != 0: + raise ValueError(f"dimension {x.size} cannot be divided by window {window}") + + items = int(x.size/window) + + zip_x, zip_y = base_uniform(np.zeros(items), np.ones(items)) + + ofs1 = np.empty(x.size) + ofs2 = np.empty(x.size) + for i in range(items): + sls = slice(i*window, (i+1)*window, 1) + if zip_x[i] == 0: + ofs1[sls] = x[sls] + ofs2[sls] = y[sls] + else: + ofs2[sls] = x[sls] + ofs1[sls] = y[sls] + + return ofs1, ofs2 + + return func + + # + # + # ONLY FOR REAL VARIABLES + # + # + + @staticmethod + def arithmetic() -> CrossoverFunc: + + def func(x: array1D, y: array1D): + b = random.random() + a = 1-b + return a*x + b*y, a*y + b*x + + return func + + @staticmethod + def mixed(alpha: float = 0.5) -> CrossoverFunc: + + def func(x: array1D, y: array1D): + + a = np.empty(x.size) + b = np.empty(y.size) + + x_min = np.minimum(x, y) + x_max = np.maximum(x, y) + delta = alpha*(x_max-x_min) + + for i in range(x.size): + a[i] = np.random.uniform(x_min[i] - delta[i], x_max[i] + delta[i]) + b[i] = np.random.uniform(x_min[i] + delta[i], x_max[i] - delta[i]) + + return a, b + + return func + + + diff --git a/geneticalgorithm2/files.py b/geneticalgorithm2/files.py new file mode 100644 index 0000000..30563ca --- /dev/null +++ b/geneticalgorithm2/files.py @@ -0,0 +1,27 @@ + + +from pathlib import Path + +from .aliases import PathLike + + +def _mkdir(path: Path): + path.mkdir(parents=True, exist_ok=True) + + +def mkdir_of_file(file_path: PathLike): + """ + для этого файла создаёт папку, в которой он должен лежать + """ + _mkdir(Path(file_path).parent) + + +def mkdir(path: PathLike): + """mkdir with parents""" + _mkdir(Path(path)) + + +def touch(path: PathLike): + """makes empty file, makes directories for this file automatically""" + mkdir_of_file(path) + Path(path).touch() diff --git a/geneticalgorithm2/geneticalgorithm2.py b/geneticalgorithm2/geneticalgorithm2.py new file mode 100644 index 0000000..eff8b47 --- /dev/null +++ b/geneticalgorithm2/geneticalgorithm2.py @@ -0,0 +1,1221 @@ +""" +Copyright 2021 Demetry Pascal (forked from Ryan (Mohammad) Solgi) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +from typing import Callable, List, Tuple, Optional, Dict, Any, Union, Sequence, Set, Literal +from typing_extensions import TypeAlias + +import collections +import warnings +import operator + + +import sys +import time +import random +import math + +import numpy as np + +#region INTERNAL IMPORTS + +from .aliases import array1D, array2D + +from .classes import AlgorithmParams, Generation, MiddleCallbackData, GAResult, GenerationConvertible + +from .initializer import Population_initializer +from .plotting_tools import plot_pop_scores, plot_several_lines + +from .utils import can_be_prob, is_numpy, is_current_gen_number, fast_min, random_indexes_pair + +from .callbacks import MiddleCallbackFunc, CallbackFunc + +#endregion + +#region ALIASES + +VARIABLE_TYPE: TypeAlias = Literal['int', 'real', 'bool'] + +#endregion + + +class geneticalgorithm2: + + """ + Genetic Algorithm (Elitist version) for Python + + An implementation of elitist genetic algorithm for solving problems with + continuous, integers, or mixed variables. + + repo path https://github.com/PasaOpasen/geneticalgorithm2 + """ + + default_params = AlgorithmParams() + PROGRESS_BAR_LEN = 20 + + @property + def output_dict(self): + warnings.warn( + "'output_dict' is deprecated and will be removed at version 7 \n use 'result' instead" + ) + return self.result + + @property + def needs_mutation(self) -> bool: + return self.prob_mut > 0 or self.prob_mut_discrete > 0 + + #region INIT + + def __init__( + self, + function: Callable[[array1D], float], + + dimension: int, + variable_type: Union[VARIABLE_TYPE, Sequence[VARIABLE_TYPE]] = 'bool', + variable_boundaries: Optional[Union[array2D, Sequence[Tuple[float, float]]]] = None, + + variable_type_mixed=None, + + function_timeout: Optional[float] = None, + algorithm_parameters: Union[AlgorithmParams, Dict[str, Any]] = default_params + ): + """ + Args: + function - the given objective function to be minimized + #NOTE: This implementation minimizes the given objective function. + (For maximization multiply function by a negative sign: the absolute + value of the output would be the actual objective function) + + dimension - the number of decision variables + + variable_type - 'bool' if all variables are Boolean; + 'int' if all variables are integer; and 'real' if all variables are + real value or continuous. For mixed types use sequence of string of type for each variable + + variable_boundaries - Default None; leave it + None if variable_type is 'bool'; otherwise provide an array of tuples + of length two as boundaries for each variable; + the length of the array must be equal dimension. For example, + np.array([0,100],[0,200]) determines lower boundary 0 and upper boundary 100 for first + and upper boundary 200 for second variable where dimension is 2. + + variable_type_mixed -- deprecated + + function_timeout - if the given function does not provide + output before function_timeout (unit is seconds) the algorithm raise error. + For example, when there is an infinite loop in the given function. `None` means disabling + + algorithm_parameters : + @ max_num_iteration - stoping criteria of the genetic algorithm (GA) + @ population_size + @ mutation_probability + @ elit_ratio + @ crossover_probability + @ parents_portion + @ crossover_type - Default is 'uniform'; 'one_point' or 'two_point' (not only) are other options + @ mutation_type - Default is 'uniform_by_x'; see GitHub to check other options + @ mutation_discrete_type - mutation type for discrete variables + @ selection_type - Default is 'roulette'; see GitHub to check other options + @ max_iteration_without_improv - maximum number of successive iterations without improvement. If None it is ineffective + + for more details and examples of implementation please visit: + https://github.com/PasaOpasen/geneticalgorithm2 + + """ + + # all default fields + + self.param: AlgorithmParams = None + # self.crossover: Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]] = None + # self.real_mutation: Callable[[float, float, float], float] = None + # self.discrete_mutation: Callable[[int, int, int], int] = None + # self.selection: Callable[[np.ndarray, int], np.ndarray] = None + + self.f: Callable[[array1D], float] = None + self.funtimeout: float = None + self.set_function: Callable[[np.ndarray], np.ndarray] = None + + # self.dim: int = None + self.var_bounds: List[Tuple[Union[int, float], Union[int, float]]] = None + # self.indexes_int: np.ndarray = None + # self.indexes_float: np.ndarray = None + + self.checked_reports: List[Tuple[str, Callable[[array1D], None]]] = None + + self.population_size: int = None + self.progress_stream = None + + # input algorithm's parameters + + assert isinstance(algorithm_parameters, (dict, AlgorithmParams)), ( + "algorithm_parameters must be dict or AlgorithmParams object" + ) + if not isinstance(algorithm_parameters, AlgorithmParams): + algorithm_parameters = AlgorithmParams.from_dict(algorithm_parameters) + + algorithm_parameters.check_if_valid() + self.param = algorithm_parameters + self.crossover, self.real_mutation, self.discrete_mutation, self.selection = algorithm_parameters.get_CMS_funcs() + + # dimension and area bounds + self.dim = int(dimension) + assert self.dim > 0, f"dimension count must be int and >0, gotten {dimension}" + + if variable_type_mixed is not None: + warnings.warn( + f"argument variable_type_mixed is deprecated and will be removed at version 7\n " + f"use variable_type={tuple(variable_type_mixed)} instead" + ) + variable_type = variable_type_mixed + self._set_types_indexes(variable_type) # types indexes + self._set_var_boundaries(variable_type, variable_boundaries) # input variables' boundaries + + # fix mutation probs + + assert can_be_prob(self.param.mutation_probability) + self.prob_mut = self.param.mutation_probability + assert self.param.mutation_discrete_probability is None or can_be_prob(self.param.mutation_discrete_probability) + self.prob_mut_discrete = self.param.mutation_discrete_probability or self.prob_mut + + if self.param.crossover_probability is not None: + warnings.warn( + f"crossover_probability is deprecated and will be removed in version 7. " + f"Reason: it's old and has no sense" + ) + + ############################################################# + # input function + assert (callable(function)), "function must be callable!" + self.f = function + + if function_timeout is not None and function_timeout > 0: + try: + from func_timeout import func_timeout, FunctionTimedOut + except ModuleNotFoundError: + raise ModuleNotFoundError( + "function_timeout > 0 needs additional package func_timeout\n" + "run `python -m pip install func_timeout`\n" + "or disable this parameter: function_timeout=None" + ) + + self.funtimeout = None if function_timeout is None else float(function_timeout) + + ############################################################# + + self.population_size = int(self.param.population_size) + self._set_parents_count(self.param.parents_portion) + self._set_elit_count(self.population_size, self.param.elit_ratio) + assert self.parents_count >= self.elit_count, ( + f"\n number of parents ({self.parents_count}) must be greater than number of elits ({self.elit_count})" + ) + + self._set_max_iterations() + + self._set_report() + + # specify this function to speed up or change default behaviour + self.fill_children: Optional[Callable[[array2D, int], None]] = None + """ + custom function which adds children for population POP + where POP[:parents_count] are parents lines and next lines are for children + """ + + def _set_types_indexes(self, variable_type: Union[str, Sequence[str]]): + + indexes = np.arange(self.dim) + self.indexes_int = np.array([]) + self.indexes_float = np.array([]) + + assert_message = ( + f"\n variable_type must be 'bool', 'int', 'real' or a sequence with 'int' and 'real', got {variable_type}" + ) + + if isinstance(variable_type, str): + assert (variable_type in VARIABLE_TYPE.__args__), assert_message + if variable_type == 'real': + self.indexes_float = indexes + else: + self.indexes_int = indexes + + else: # sequence case + + assert len(variable_type) == self.dim, ( + f"\n variable_type must have a length equal dimension. " + f"Should be {self.dim}, got {len(variable_type)}" + ) + assert 'bool' not in variable_type, ( + "don't use 'bool' if variable_type is a sequence, " + "for 'boolean' case use 'int' and specify boundary as (0,1)" + ) + assert all(v in VARIABLE_TYPE.__args__ for v in variable_type), assert_message + + vartypes = np.array(variable_type) + self.indexes_int = indexes[vartypes == 'int'] + self.indexes_float = indexes[vartypes == 'real'] + + def _set_var_boundaries( + self, + variable_type: Union[str, Sequence[str]], + variable_boundaries + ): + if isinstance(variable_type, str) and variable_type == 'bool': + self.var_bounds = [(0, 1)] * self.dim + else: + + if is_numpy(variable_boundaries): + assert variable_boundaries.shape == (self.dim, 2), ( + f"\n if variable_boundaries is numpy array, it must be with shape (dim, 2)" + ) + else: + assert len(variable_boundaries) == self.dim and all((len(t) == 2 for t in variable_boundaries)), ( + "\n if variable_boundaries is sequence, " + "it must be with len dim and boundary for each variable must be a tuple of length two" + ) + + for i in variable_boundaries: + assert i[0] <= i[1], "\n lower_boundaries must be smaller than upper_boundaries [lower,upper]" + + self.var_bounds = [(i[0], i[1]) for i in variable_boundaries] + + def _set_parents_count(self, parents_portion: float): + + self.parents_count = int(parents_portion * self.population_size) + assert self.population_size >= self.parents_count > 1, ( + f'parents count {self.parents_count} cannot be less than population size {self.population_size}' + ) + trl = self.population_size - self.parents_count + if trl % 2 != 0: + self.parents_count += 1 + + def _set_elit_count(self, pop_size: int, elit_ratio: Union[float, int]): + + assert elit_ratio >= 0 + self.elit_count = elit_ratio if isinstance(elit_ratio, str) else math.ceil(pop_size*elit_ratio) + + def _set_max_iterations(self): + + if self.param.max_num_iteration is None: + iterate = 0 + for i in range(0, self.dim): + bound_min, bound_max = self.var_bounds[i] + var_space = bound_max - bound_min + if i in self.indexes_int: + iterate += var_space * self.dim * (100 / self.population_size) + else: + iterate += var_space * 50 * (100 / self.population_size) + iterate = int(iterate) + if (iterate * self.population_size) > 10000000: + iterate = 10000000 / self.population_size + + self.max_iterations = fast_min(iterate, 8000) + else: + assert self.param.max_num_iteration > 0 + self.max_iterations = math.ceil(self.param.max_num_iteration) + + max_it = self.param.max_iteration_without_improv + if max_it is None: + self.max_stagnations = self.max_iterations + 1 + else: + self.max_stagnations = math.ceil(max_it) + + #endregion + + #region REPORT + + def _set_report(self): + """ + creates default report checker + """ + self.checked_reports = [ + # item 0 cuz scores will be sorted and min item is items[0] + ('report', operator.itemgetter(0)) + ] + + def _clear_report(self): + """ + removes all report objects + """ + fields = [f for f in vars(self).keys() if f.startswith('report')] + for attr in fields: + delattr(self, attr) + + def _init_report(self): + """ + makes empty report fields + """ + for name, _ in self.checked_reports: + setattr(self, name, []) + + def _update_report(self, scores: array1D): + """ + append report value to the end of field + """ + for name, func in self.checked_reports: + getattr(self, name).append( + func(scores) + ) + + #endregion + + #region RUN METHODS + + def _progress(self, count: int, total: int, status: str = ''): + + part = count / total + + filled_len = round(geneticalgorithm2.PROGRESS_BAR_LEN * part) + percents = round(100.0 * part, 1) + bar = '|' * filled_len + '_' * (geneticalgorithm2.PROGRESS_BAR_LEN - filled_len) + + self.progress_stream.write('\r%s %s%s %s' % (bar, percents, '%', status)) + self.progress_stream.flush() + + def __str__(self): + return f"Genetic algorithm object with parameters {self.param}" + + def __repr__(self): + return self.__str__() + + def _simulate(self, sample: array1D): + + from func_timeout import func_timeout, FunctionTimedOut + + obj = None + eval_time = time.time() + try: + obj = func_timeout( + self.funtimeout, + lambda: self.f(sample) + ) + except FunctionTimedOut: + print("given function is not applicable") + eval_time = time.time() - eval_time + + assert obj is not None, ( + f"the given function was running like {eval_time} seconds and does not provide any output" + ) + + tp = type(obj) + assert ( + tp in (int, float) or np.issubdtype(tp, np.floating) or np.issubdtype(tp, np.integer) + ), f"Minimized function should return a number, but got '{obj}' object with type {tp}" + + return obj, eval_time + + def _set_mutation_indexes(self, mutation_indexes: Optional[Sequence[int]]): + + if mutation_indexes is None: + self.indexes_float_mut = self.indexes_float + self.indexes_int_mut = self.indexes_int + else: + tmp_indexes = set(mutation_indexes) + self.indexes_int_mut = np.array(list(set(self.indexes_int).intersection(tmp_indexes))) + self.indexes_float_mut = np.array(list(set(self.indexes_float).intersection(tmp_indexes))) + + if self.indexes_float_mut.size == 0 and self.indexes_int_mut.size == 0: + warnings.warn(f"No mutation dimensions!!! Check ur mutation indexes!!") + + #@profile + def run( + self, + no_plot: bool = False, + disable_printing: bool = False, + progress_bar_stream: Optional[str] = 'stdout', + + # deprecated + disable_progress_bar: bool = False, + + set_function: Optional[Callable[[array2D], array1D]] = None, + apply_function_to_parents: bool = False, + start_generation: GenerationConvertible = Generation(), + studEA: bool = False, + mutation_indexes: Optional[Union[Sequence[int], Set[int]]] = None, + + init_creator: Optional[Callable[[], array1D]] = None, + init_oppositors: Optional[Sequence[Callable[[array1D], array1D]]] = None, + + duplicates_oppositor: Optional[Callable[[array1D], array1D]] = None, + remove_duplicates_generation_step: Optional[int] = None, + + revolution_oppositor: Optional[Callable[[array1D], array1D]] = None, + revolution_after_stagnation_step: Optional[int] = None, + revolution_part: float = 0.3, + + population_initializer: Tuple[ + int, Callable[[array2D, array1D], Tuple[array2D, array1D]] + ] = Population_initializer(select_best_of=1, local_optimization_step='never', local_optimizer=None), + + stop_when_reached: Optional[float] = None, + callbacks: Optional[Sequence[CallbackFunc]] = None, + middle_callbacks: Optional[Sequence[MiddleCallbackFunc]] = None, #+ + time_limit_secs: Optional[float] = None, + save_last_generation_as: Optional[str] = None, + seed: Optional[int] = None + ): + """ + runs optimization process + + Args: + no_plot: do not plot results using matplotlib by default + + disable_printing: do not print log info of optimization process + + progress_bar_stream: 'stdout', 'stderr' or None to disable progress bar + + disable_progress_bar: + + set_function : 2D-array -> 1D-array function, + which applyes to matrix of population (size (samples, dimention)) + to estimate their values + + apply_function_to_parents: apply function to parents from previous generation (if it's needed) + + start_generation: Generation object or a dictionary with structure + {'variables':2D-array of samples, 'scores': function values on samples} + or path to .npz file (str) with saved generation; if 'scores' value is None the scores will be compute + + studEA: using stud EA strategy (crossover with best object always) + + mutation_indexes: indexes of dimensions where mutation can be performed (all dimensions by default) + + init_creator: the function creates population samples. + By default -- random uniform for real variables and random uniform for int + init_oppositors: the list of oppositors creates oppositions for base population. No by default + duplicates_oppositor: oppositor for applying after duplicates removing. + By default -- using just random initializer from creator + remove_duplicates_generation_step: step for removing duplicates (have a sense with discrete tasks). + No by default + revolution_oppositor: oppositor for revolution time. No by default + revolution_after_stagnation_step: create revolution after this generations of stagnation. No by default + revolution_part: float, the part of generation to being oppose. By default is 0.3 + + population_initializer: object for actions at population initialization step + to create better start population. See doc + + stop_when_reached: stop searching after reaching this value (it can be potential minimum or something else) + + callbacks: sequence of callback functions with structure: + (generation_number, report_list, last_population, last_scores) -> do some action + + middle_callbacks: sequence of functions made MiddleCallbacks class + + time_limit_secs: limit time of working (in seconds) + + save_last_generation_as: path to .npz file for saving last_generation as numpy dictionary like + {'population': 2D-array, 'scores': 1D-array}, None if doesn't need to save in file + + seed: random seed (None if doesn't matter) + """ + + if disable_progress_bar: + warnings.warn( + f"disable_progress_bar is deprecated and will be removed in version 7, " + f"use probress_bar_stream=None to disable progress bar" + ) + progress_bar_stream = None + + enable_printing: bool = not disable_printing + + start_generation = Generation.from_object(self.dim, start_generation) + + assert is_current_gen_number(revolution_after_stagnation_step), "must be None or int and >0" + assert is_current_gen_number(remove_duplicates_generation_step), "must be None or int and >0" + assert can_be_prob(revolution_part), f"revolution_part must be in [0,1], not {revolution_part}" + assert stop_when_reached is None or isinstance(stop_when_reached, (int, float)) + assert isinstance(callbacks, collections.abc.Sequence) or callbacks is None, ( + "callbacks should be a list of callbacks functions" + ) + assert isinstance(middle_callbacks, collections.abc.Sequence) or middle_callbacks is None, ( + "middle_callbacks should be list of MiddleCallbacks functions" + ) + assert time_limit_secs is None or time_limit_secs > 0, 'time_limit_secs must be None of number > 0' + + self._set_mutation_indexes(mutation_indexes) + from OppOpPopInit import set_seed + set_seed(seed) + + # randomstate = np.random.default_rng(random.randint(0, 1000) if seed is None else seed) + # self.randomstate = randomstate + + # using bool flag is faster than using empty function with generated string arguments + SHOW_PROGRESS = progress_bar_stream is not None + if SHOW_PROGRESS: + + show_progress = lambda t, t2, s: self._progress(t, t2, status=s) + + if progress_bar_stream == 'stdout': + self.progress_stream = sys.stdout + elif progress_bar_stream == 'stderr': + self.progress_stream = sys.stderr + else: + raise Exception( + f"wrong value {progress_bar_stream} of progress_bar_stream, must be 'stdout'/'stderr'/None" + ) + else: + show_progress = None + + stop_by_val = ( + (lambda best_f: False) + if stop_when_reached is None + else (lambda best_f: best_f <= stop_when_reached) + ) + + t: int = 0 + count_stagnation: int = 0 + pop: array2D = None + scores: array1D = None + + # + # + # callbacks part + # + # + + def get_data(): + """ + returns all important data about model + """ + return MiddleCallbackData( + last_generation=Generation(pop, scores), + current_generation=t, + report_list=self.report, + + mutation_prob=self.prob_mut, + mutation_discrete_prob=self.prob_mut_discrete, + mutation=self.real_mutation, + mutation_discrete=self.discrete_mutation, + crossover=self.crossover, + selection=self.selection, + + current_stagnation=count_stagnation, + max_stagnation=self.max_stagnations, + + parents_portion=self.param.parents_portion, + elit_ratio=self.param.elit_ratio, + + set_function=self.set_function, + + reason_to_stop=reason_to_stop + ) + + def set_data(data: MiddleCallbackData): + """ + sets data to model + """ + nonlocal pop, scores, count_stagnation, reason_to_stop + + pop, scores = data.last_generation.variables, data.last_generation.scores + self.population_size = pop.shape[0] + + self.param.parents_portion = data.parents_portion + self._set_parents_count(data.parents_portion) + + self.param.elit_ratio = data.elit_ratio + self._set_elit_count(self.population_size, data.elit_ratio) + + self.prob_mut = data.mutation_prob + self.prob_mut_discrete = data.mutation_discrete_prob + self.real_mutation = data.mutation + self.discrete_mutation = data.mutation_discrete + self.crossover = data.crossover + self.selection = data.selection + + count_stagnation = data.current_stagnation + reason_to_stop = data.reason_to_stop + self.max_stagnations = data.max_stagnation + + self.set_function = data.set_function + + if not callbacks: + total_callback = lambda g, r, lp, ls: None + else: + def total_callback( + generation_number: int, + report_list: List[float], + last_population: array2D, + last_scores: array1D + ): + for cb in callbacks: + cb(generation_number, report_list, last_population, last_scores) + + if not middle_callbacks: + total_middle_callback = lambda: None + else: + def total_middle_callback(): + """ + applies callbacks and sets new data if there is a sence + """ + data = get_data() + flag = False + for cb in middle_callbacks: + data, has_sense = cb(data) + if has_sense: + flag = True + if flag: + set_data(data) # update global date if there was real callback step + + ############################################################ + + start_time = time.time() + time_is_done = ( + (lambda: False) + if time_limit_secs is None + else (lambda: int(time.time() - start_time) >= time_limit_secs) + ) + + ############################################################# + # Initial Population + self.set_function = set_function or geneticalgorithm2.default_set_function(self.f) + + pop_coef, initializer_func = population_initializer + + # population creator by random or with oppositions + if init_creator is None: + + from OppOpPopInit import SampleInitializers + + # just uniform random + self.creator = SampleInitializers.Combined( + minimums=[v[0] for v in self.var_bounds], + maximums=[v[1] for v in self.var_bounds], + indexes=( + self.indexes_int, + self.indexes_float + ), + creator_initializers=( + SampleInitializers.RandomInteger, + SampleInitializers.Uniform + ) + ) + else: + assert callable(init_creator) + self.creator = init_creator + + self.init_oppositors = init_oppositors + self.dup_oppositor = duplicates_oppositor + self.revolution_oppositor = revolution_oppositor + + # event for removing duplicates + if remove_duplicates_generation_step is None: + def remover(pop: array2D, scores: array1D, gen: int) -> Tuple[ + array2D, + array1D + ]: + return pop, scores + else: + + def without_dup(pop: array2D, scores: array1D): # returns population without dups + _, index_of_dups = np.unique(pop, axis=0, return_index=True) + return pop[index_of_dups], scores[index_of_dups], scores.size - index_of_dups.size + + if self.dup_oppositor is None: # if there is no dup_oppositor, use random creator + def remover(pop: array2D, scores: array1D, gen: int) -> Tuple[ + array2D, + array1D + ]: + if gen % remove_duplicates_generation_step != 0: + return pop, scores + + pp, sc, count_to_create = without_dup(pop, scores) # pop without dups + + if count_to_create == 0: + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}. No dups!") + return pop, scores + + pp2 = SampleInitializers.CreateSamples(self.creator, count_to_create) # new pop elements + pp2_scores = self.set_function(pp2) # new elements values + + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}. Kill dups!") + + new_pop = np.vstack((pp, pp2)) + new_scores = np.concatenate((sc, pp2_scores)) + + args_to_sort = new_scores.argsort() + return new_pop[args_to_sort], new_scores[args_to_sort] + + else: # using oppositors + assert callable(self.dup_oppositor) + + def remover(pop: np.ndarray, scores: np.ndarray, gen: int) -> Tuple[ + np.ndarray, + np.ndarray + ]: + if gen % remove_duplicates_generation_step != 0: + return pop, scores + + pp, sc, count_to_create = without_dup(pop, scores) # pop without dups + + if count_to_create == 0: + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}. No dups!") + return pop, scores + + if count_to_create > sc.size: + raise Exception( + f"Too many duplicates at generation {gen} ({count_to_create} > {sc.size}), cannot oppose" + ) + + # oppose count_to_create worse elements + pp2 = OppositionOperators.Reflect(pp[-count_to_create:], self.dup_oppositor) # new pop elements + pp2_scores = self.set_function(pp2) # new elements values + + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}. Kill dups!") + + new_pop = np.vstack((pp, pp2)) + new_scores = np.concatenate((sc, pp2_scores)) + + args_to_sort = new_scores.argsort() + return new_pop[args_to_sort], new_scores[args_to_sort] + + # event for revolution + if revolution_after_stagnation_step is None: + def revolution(pop: array2D, scores: array1D, stagnation_count: int) -> Tuple[ + array2D, + array1D + ]: + return pop, scores + else: + if revolution_oppositor is None: + raise Exception( + f"How can I make revolution each {revolution_after_stagnation_step} stagnation steps " + f"if revolution_oppositor is None (not defined)?" + ) + assert callable(revolution_oppositor) + + from OppOpPopInit import OppositionOperators + + def revolution(pop: array2D, scores: array1D, stagnation_count: int) -> Tuple[ + array2D, + array1D + ]: + if stagnation_count < revolution_after_stagnation_step: + return pop, scores + part = int(pop.shape[0]*revolution_part) + + pp2 = OppositionOperators.Reflect(pop[-part:], self.revolution_oppositor) + pp2_scores = self.set_function(pp2) + + combined = np.vstack((pop, pp2)) + combined_scores = np.concatenate((scores, pp2_scores)) + args = combined_scores.argsort() + + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}. Revolution!") + + args = args[:scores.size] + return combined[args], combined_scores[args] + + # + # + # START ALGORITHM LOGIC + # + # + + # Report + self._clear_report() # clear old report objects + self._init_report() + + # initialization of pop + + if start_generation.variables is None: + + from OppOpPopInit import init_population + + real_pop_size = self.population_size * pop_coef + + # pop = np.empty((real_pop_size, self.dim)) + scores = np.empty(real_pop_size) + + pop = init_population( + samples_count=real_pop_size, + creator=self.creator, + oppositors=self.init_oppositors + ) + + if self.funtimeout and self.funtimeout > 0: # perform simulation + + time_counter = 0 + + for p in range(0, real_pop_size): + # simulation returns exception or func value -- check the time of evaluating + value, eval_time = self._simulate(pop[p]) + scores[p] = value + time_counter += eval_time + + if enable_printing: + print( + f"\nSim: Average time of function evaluating (secs): " + f"{time_counter/real_pop_size} (total = {time_counter})\n" + ) + else: + + eval_time = time.time() + scores = self.set_function(pop) + eval_time = time.time() - eval_time + if enable_printing: + print( + f"\nSet: Average time of function evaluating (secs): " + f"{eval_time/real_pop_size} (total = {eval_time})\n" + ) + + else: + + self.population_size = start_generation.variables.shape[0] + self._set_elit_count(self.population_size, self.param.elit_ratio) + self._set_parents_count(self.param.parents_portion) + + pop = start_generation.variables + + if start_generation.scores is None: + + _time = time.time() + scores = self.set_function(pop) + _time = time.time() - _time + + if enable_printing: + print( + f'\nFirst scores are made from gotten variables ' + f'(by {_time} secs, about {_time/scores.size} for each creature)\n' + ) + else: + scores = start_generation.scores + if enable_printing: + print(f"\nFirst scores are from gotten population\n") + + # Initialization by select bests and local_descent + + pop, scores = initializer_func(pop, scores) + + # first sort + args_to_sort = scores.argsort() + pop = pop[args_to_sort] + scores = scores[args_to_sort] + self._update_report(scores) + + self.population_size = scores.size + self.best_function = scores[0] + + if enable_printing: + print( + f"Best score before optimization: {self.best_function}" + ) + + t: int = 1 + count_stagnation: int = 0 + """iterations without progress""" + reason_to_stop: Optional[str] = None + + # gets indexes of 2 parents to crossover + if studEA: + get_parents_inds = lambda: (0, random.randrange(1, self.parents_count)) + else: + get_parents_inds = lambda: random_indexes_pair(self.parents_count) + + # while no reason to stop + while True: + + if count_stagnation > self.max_stagnations: + reason_to_stop = f"limit of fails: {count_stagnation}" + elif t == self.max_iterations: + reason_to_stop = f'limit of iterations: {t}' + elif stop_by_val(self.best_function): + reason_to_stop = f"stop value reached: {self.best_function} <= {stop_when_reached}" + elif time_is_done(): + reason_to_stop = f'time is done: {time.time() - start_time} >= {time_limit_secs}' + + if reason_to_stop is not None: + if SHOW_PROGRESS: + show_progress(t, self.max_iterations, f"GA is running... STOP! {reason_to_stop}") + break + + if scores[0] < self.best_function: # if there is progress + count_stagnation = 0 + self.best_function = scores[0] + else: + count_stagnation += 1 + + if SHOW_PROGRESS: + show_progress( + t, + self.max_iterations, + f"GA is running...{t} gen from {self.max_iterations}...best value = {self.best_function}" + ) + + # Select parents + + par: array2D = np.empty((self.parents_count, self.dim)) + """samples chosen to create new samples""" + par_scores: array1D = np.empty(self.parents_count) + + elit_slice = slice(None, self.elit_count) + """elit parents""" + + # copy needs because the generation will be removed after parents selection + par[elit_slice] = pop[elit_slice].copy() + par_scores[elit_slice] = scores[elit_slice].copy() + + new_par_inds = ( + self.selection( + scores[self.elit_count:], + self.parents_count - self.elit_count + ).astype(np.int16) + self.elit_count + ) + """non-elit parents indexes""" + #new_par_inds = self.selection(scores, self.parents_count - self.elit_count).astype(np.int16) + par_slice = slice(self.elit_count, self.parents_count) + par[par_slice] = pop[new_par_inds].copy() + par_scores[par_slice] = scores[new_par_inds].copy() + + pop = np.empty((self.population_size, self.dim)) + """new generation""" + scores = np.empty(self.population_size) + """new generation scores""" + + parents_slice = slice(None, self.parents_count) + pop[parents_slice] = par + scores[parents_slice] = par_scores + + if self.fill_children is None: # default fill children behaviour + DO_MUTATION = self.needs_mutation + for k in range(self.parents_count, self.population_size, 2): + + r1, r2 = get_parents_inds() + + pvar1 = pop[r1] # equal to par[r1], but better for cache + pvar2 = pop[r2] + + ch1, ch2 = self.crossover(pvar1, pvar2) + + if DO_MUTATION: + ch1 = self.mut(ch1) + ch2 = self.mut_middle(ch2, pvar1, pvar2) + + pop[k] = ch1 + pop[k+1] = ch2 + else: # custom behaviour + self.fill_children(pop, self.parents_count) + + if apply_function_to_parents: + scores = self.set_function(pop) + else: + scores[self.parents_count:] = self.set_function(pop[self.parents_count:]) + + # remove duplicates + pop, scores = remover(pop, scores, t) + # revolution + pop, scores = revolution(pop, scores, count_stagnation) + + # make population better + args_to_sort = scores.argsort() + pop = pop[args_to_sort] + scores = scores[args_to_sort] + self._update_report(scores) + + # callback it + total_callback(t, self.report, pop, scores) + total_middle_callback() + + t += 1 + + self.best_function = fast_min(scores[0], self.best_function) + + last_generation = Generation(pop, scores) + self.result = GAResult(last_generation) + + if save_last_generation_as is not None: + last_generation.save(save_last_generation_as) + + if enable_printing: + show = ' ' * 200 + sys.stdout.write( + f'\r{show}\n' + f'\r The best found solution:\n {pop[0]}' + f'\n\n Objective function:\n {self.best_function}\n' + f'\n Used generations: {len(self.report)}' + f'\n Used time: {time.time() - start_time:.3g} seconds\n' + ) + sys.stdout.flush() + + if not no_plot: + self.plot_results() + + if enable_printing: + if reason_to_stop is not None and 'iterations' not in reason_to_stop: + sys.stdout.write( + f'\nWarning: GA is terminated because of {reason_to_stop}' + ) + + return self.result + + #endregion + + #region PLOTTING + + def plot_results( + self, + title: str = 'Genetic Algorithm', + save_as: Optional[str] = None, + dpi: int = 200, + main_color: str = 'blue' + ): + """ + Simple plot of self.report (if not empty) + """ + if len(self.report) == 0: + sys.stdout.write("No results to plot!\n") + return + + plot_several_lines( + lines=[self.report], + colors=[main_color], + labels=['best of generation'], + linewidths=[2], + title=title, + xlabel='Generation', + ylabel='Minimized function', + save_as=save_as, + dpi=dpi + ) + + def plot_generation_scores(self, title: str = 'Last generation scores', save_as: Optional[str] = None): + """ + Plots barplot of scores of last population + """ + + if not hasattr(self, 'result'): + raise Exception( + "There is no 'result' field into ga object! Before plotting generation u need to run seaching process" + ) + + plot_pop_scores(self.result.last_generation.scores, title, save_as) + + #endregion + + #region MUTATION + def mut(self, x: array1D): + """ + just mutation + """ + + for i in self.indexes_int_mut: + if random.random() < self.prob_mut_discrete: + x[i] = self.discrete_mutation(x[i], *self.var_bounds[i]) + + for i in self.indexes_float_mut: + if random.random() < self.prob_mut: + x[i] = self.real_mutation(x[i], *self.var_bounds[i]) + + return x + + def mut_middle(self, x: array1D, p1: array1D, p2: array1D): + """ + mutation oriented on parents + """ + for i in self.indexes_int_mut: + + if random.random() < self.prob_mut_discrete: + v1, v2 = p1[i], p2[i] + if v1 < v2: + x[i] = random.randint(v1, v2) + elif v1 > v2: + x[i] = random.randint(v2, v1) + else: + x[i] = random.randint(*self.var_bounds[i]) + + for i in self.indexes_float_mut: + if random.random() < self.prob_mut: + v1, v2 = p1[i], p2[i] + if v1 != v2: + x[i] = random.uniform(v1, v2) + else: + x[i] = random.uniform(*self.var_bounds[i]) + return x + + #endregion + + #region Set functions + + @staticmethod + def default_set_function(function_for_set: Callable[[array1D], float]): + """ + simple function for creating set_function + function_for_set just applies to each row of population + """ + def func(matrix: array2D): + return np.array( + [function_for_set(row) for row in matrix] + ) + return func + + @staticmethod + def vectorized_set_function(function_for_set: Callable[[array1D], float]): + """ + works like default, but faster for big populations and slower for little + function_for_set just applyes to each row of population + """ + + func = np.vectorize(function_for_set, signature='(n)->()') + return func + + @staticmethod + def set_function_multiprocess(function_for_set: Callable[[array1D], float], n_jobs: int = -1): + """ + like function_for_set but uses joblib with n_jobs (-1 goes to count of available processors) + """ + try: + from joblib import Parallel, delayed + except ModuleNotFoundError: + raise ModuleNotFoundError( + "this additional feature requires joblib package," + "run `pip install joblib` or `pip install --upgrade geneticalgorithm2[full]`" + "or use another set function" + ) + + def func(matrix: array2D): + result = Parallel(n_jobs=n_jobs)(delayed(function_for_set)(matrix[i]) for i in range(matrix.shape[0])) + return np.array(result) + return func + + #endregion + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/geneticalgorithm2/initializer.py b/geneticalgorithm2/initializer.py new file mode 100644 index 0000000..a302fc4 --- /dev/null +++ b/geneticalgorithm2/initializer.py @@ -0,0 +1,90 @@ + +from typing import Callable, Optional, Tuple, Literal + +import numpy as np + +from .aliases import TypeAlias, array1D, array2D + + +LOCAL_OPT_STEPS: TypeAlias = Literal['before_select', 'after_select', 'never'] + + +def Population_initializer( + select_best_of: int = 4, + local_optimization_step: LOCAL_OPT_STEPS = 'never', + local_optimizer: Optional[ + Callable[ + [array1D, float], + Tuple[array1D, float] + ] + ] = None +) -> Tuple[int, Callable[[array2D, array1D], Tuple[array2D, array1D]]]: + """ + select_best_of (int) -- select 1/select_best_of best part of start population. For example, for select_best_of = 4 and population_size = N will be selected N best objects from 5N generated objects (if start_generation = None dictionary). If start_generation is not None dictionary, it will be selected best size(start_generation)/N objects + + local_optimization_step (str) -- when should we do local optimization? Available values: + + * 'never' -- don't do local optimization + * 'before_select' -- before selection best N objects (example: do local optimization for 5N objects and select N best results) + * 'after_select' -- do local optimization on best selected N objects + + local_optimizer (function) -- local optimization function like: + def loc_opt(object_as_array, current_score): + # some code + return better_object_as_array, better_score + """ + + assert (select_best_of > 0 and type(select_best_of) == int), "select_best_of argument should be integer and more than 0" + + assert (local_optimization_step in LOCAL_OPT_STEPS.__args__), f"local_optimization_step should be in {LOCAL_OPT_STEPS.__args__}, but got {local_optimization_step}" + + if local_optimizer is None and local_optimization_step in LOCAL_OPT_STEPS.__args__[:2]: + raise Exception(f"for local_optimization_step from {LOCAL_OPT_STEPS.__args__[:2]} local_optimizer function mustn't be None") + + + def Select_best(population: array2D, scores: array1D) -> Tuple[array2D, array1D]: + args = np.argsort(scores) + args = args[:round(args.size/select_best_of)] + return population[args], scores[args] + + def Local_opt(population: array2D, scores: array1D): + _pop, _score = zip( + *[ + local_optimizer(population[i], scores[i]) for i in range(scores.size) + ] + ) + return np.array(_pop), np.array(_score) + + + #def Create_population(func, start_generation, expected_size, #variable_boundaries): + # + # if not (start_generation['variables'] is None): + # pop = start_generation['variables'] + # scores = start_generation['scores'] + # if scores is None: + # scores = np.array([func(pop[i, :]) for i in range(pop.shape[0])]) + # return pop, scores + + + def Result(population: array2D, scores: array1D): + if local_optimization_step == 'before_select': + pop, s = Local_opt(population, scores) + return Select_best(pop, s) + + if local_optimization_step == 'after_select': + pop, s = Select_best(population, scores) + return Local_opt(pop, s) + + #if local_optimization_step == 'never': + return Select_best(population, scores) + + + return select_best_of, Result + + + + + + + + diff --git a/geneticalgorithm2/mutations.py b/geneticalgorithm2/mutations.py new file mode 100644 index 0000000..80b4386 --- /dev/null +++ b/geneticalgorithm2/mutations.py @@ -0,0 +1,84 @@ + +from typing import Callable, Dict, Union + +import random +import numpy as np + + +from .utils import fast_min, fast_max + +from .aliases import TypeAlias + +MutationFloatFunc: TypeAlias = Callable[[float, float, float], float] +MutationIntFunc: TypeAlias = Callable[[int, int, int], int] +MutationFunc: TypeAlias = Union[MutationIntFunc, MutationFloatFunc] + + +class Mutations: + + @staticmethod + def mutations_dict() -> Dict[str, MutationFloatFunc]: + return { + 'uniform_by_x': Mutations.uniform_by_x(), + 'uniform_by_center': Mutations.uniform_by_center(), + 'gauss_by_center': Mutations.gauss_by_center(), + 'gauss_by_x': Mutations.gauss_by_x(), + } + + @staticmethod + def mutations_discrete_dict() -> Dict[str, MutationIntFunc]: + return { + 'uniform_discrete': Mutations.uniform_discrete() + } + + + @staticmethod + def uniform_by_x() -> MutationFloatFunc: + + def func(x: float, left: float, right: float): + alp: float = fast_min(x - left, right - x) + return random.uniform(x - alp, x + alp) + return func + + @staticmethod + def uniform_by_center() -> MutationFloatFunc: + + def func(x: float, left: float, right: float): + return random.uniform(left, right) + + return func + + @staticmethod + def gauss_by_x(sd: float = 0.3) -> MutationFloatFunc: + """ + gauss mutation with x as center and sd*length_of_zone as std + """ + def func(x: float, left: float, right: float): + std: float = sd * (right - left) + return fast_max( + left, + fast_min(right, np.random.normal(loc=x, scale=std)) + ) + + return func + + @staticmethod + def gauss_by_center(sd: float = 0.3) -> MutationFloatFunc: + """ + gauss mutation with (left+right)/2 as center and sd*length_of_zone as std + """ + def func(x: float, left: float, right: float): + std: float = sd * (right - left) + return fast_max( + left, + fast_min(right, np.random.normal(loc=(left + right) * 0.5, scale=std)) + ) + + return func + + @staticmethod + def uniform_discrete() -> MutationIntFunc: + def func(x: int, left: int, right: int) -> int: + return random.randint(left, right) + return func + diff --git a/geneticalgorithm2/plotting_tools.py b/geneticalgorithm2/plotting_tools.py new file mode 100644 index 0000000..c306e78 --- /dev/null +++ b/geneticalgorithm2/plotting_tools.py @@ -0,0 +1,106 @@ + +from typing import Optional, Sequence + +import numpy as np + +from .aliases import PathLike +from .files import mkdir_of_file + + +def plot_several_lines( + lines: Sequence[Sequence[float]], + colors: Sequence[str], + labels: Optional[Sequence[str]] = None, + linewidths: Optional[Sequence[int]] = None, + title: str = '', + xlabel: str = 'Generation', + ylabel: str = 'Minimized function', + save_as: Optional[PathLike] = None, + dpi: int = 200 +): + # import matplotlib + # matplotlib.use('Agg') + import matplotlib.pyplot as plt + from matplotlib.ticker import MaxNLocator + + ax = plt.axes() + ax.xaxis.set_major_locator(MaxNLocator(integer=True)) + + if labels is None: + labels = list(map(str, range(len(lines)))) + if linewidths is None: + linewidths = [1.5] * len(lines) + + for line, color, label, linewidth in zip( + lines, colors, labels, linewidths + ): + plt.plot( + line, + color=color, + label=label, + linewidth=linewidth + ) + + plt.xlabel(xlabel) + plt.ylabel(ylabel) + plt.title(title) + plt.legend() + + if save_as is not None: + plt.savefig(save_as, dpi=dpi) + + plt.show() + +def plot_pop_scores(scores: Sequence[float], title: str = 'Population scores', save_as: Optional[str] = None): + """ + plots scores (numeric values) as sorted bars + """ + # import matplotlib + # matplotlib.use('Agg') + import matplotlib.pyplot as plt + import matplotlib.cm as cm + from matplotlib.colors import Normalize + from matplotlib.ticker import NullLocator + + sc = sorted(scores)[::-1] + + fig, ax = plt.subplots(figsize=(7, 5)) + ax.xaxis.set_major_locator(NullLocator()) + + def autolabel(rects): + """Attach a text label above each bar in *rects*, displaying its height.""" + for rect in rects[-1:]: + height = round(rect.get_height(), 2) + ax.annotate('{}'.format(height), + xy=(rect.get_x() + rect.get_width() / 2, height), + xytext=(0, 3), # 3 points vertical offset + textcoords="offset points", + ha='center', va='bottom', fontsize=14, fontweight='bold') + + + + cols = np.zeros(len(sc)) + cols[-1] = 1 + + x_coord = np.arange(len(sc)) + my_norm = Normalize(vmin=0, vmax=1) + + rc = ax.bar(x_coord, sc, width=0.7, color=cm.get_cmap('Set2')(my_norm(cols))) + + autolabel(rc) + + #ax.set_xticks(x_coord) + ax.set_title(title, fontsize=16, fontweight='bold') + ax.set_xlabel('Population objects') + ax.set_ylabel('Cost values') + #ax.set_ylim([0, max(subdict.values())*1.2]) + #fig.suptitle(title, fontsize=15, fontweight='bold') + + + fig.tight_layout() + + if save_as is not None: + mkdir_of_file(save_as) + plt.savefig(save_as, dpi=200) + + plt.show() diff --git a/geneticalgorithm2/selections.py b/geneticalgorithm2/selections.py new file mode 100644 index 0000000..8ae2278 --- /dev/null +++ b/geneticalgorithm2/selections.py @@ -0,0 +1,192 @@ + +from typing import Callable, Dict, List + +import math +import random +import numpy as np + +from .aliases import array1D, TypeAlias + +SelectionFunc: TypeAlias = Callable[[array1D, int], array1D] + + +class Selection: + + @staticmethod + def selections_dict() -> Dict[str, SelectionFunc]: + return { + 'fully_random': Selection.fully_random(), + 'roulette': Selection.roulette(), + 'stochastic': Selection.stochastic(), + 'sigma_scaling': Selection.sigma_scaling(), + 'ranking': Selection.ranking(), + 'linear_ranking': Selection.linear_ranking(), + 'tournament': Selection.tournament(), + } + + @staticmethod + def __inverse_scores(scores: array1D) -> array1D: + """ + inverse scores (min val goes to max) + """ + minobj = scores[0] + normobj = scores - minobj if minobj < 0 else scores + + return (np.amax(normobj) + 1) - normobj + + @staticmethod + def fully_random() -> SelectionFunc: + + def func(scores: array1D, parents_count: int): + indexes = np.arange(parents_count) + return np.random.choice(indexes, parents_count, replace = False) + + return func + + @staticmethod + def __roulette(scores: array1D, parents_count: int) -> array1D: + + sum_normobj = np.sum(scores) + prob = scores/sum_normobj + cumprob = np.cumsum(prob) + + parents_indexes = np.empty(parents_count) + + # it can be vectorized + for k in range(parents_count): + index = np.searchsorted(cumprob, np.random.random()) + if index < cumprob.size: + parents_indexes[k] = index + else: + parents_indexes[k] = np.random.randint(0, index - 1) + + return parents_indexes + + @staticmethod + def roulette() -> SelectionFunc: + + def func(scores: array1D, parents_count: int): + + normobj = Selection.__inverse_scores(scores) + + return Selection.__roulette(normobj, parents_count) + + return func + + @staticmethod + def stochastic() -> SelectionFunc: + + def func(scores: np.ndarray, parents_count: int): + f = Selection.__inverse_scores(scores) + + fN: float = 1.0 / parents_count + k: int = 0 + acc: float = 0.0 + parents: List[int] = [] + r: float = random.random() * fN + + while len(parents) < parents_count: + + acc += f[k] + + while acc > r: + parents.append(k) + if len(parents) == parents_count: + break + r += fN + + k += 1 + + return np.array(parents[:parents_count]) + + return func + + @staticmethod + def sigma_scaling(epsilon: float = 0.01, is_noisy: bool = False) -> SelectionFunc: + + def func(scores: array1D, parents_count): + f = Selection.__inverse_scores(scores) + + sigma = np.std(f, ddof = 1) if is_noisy else np.std(f) + average = np.mean(f) + + if sigma == 0: + f = 1 + else: + f = np.maximum(epsilon, 1 + (f - average)/(2*sigma)) + + return Selection.__roulette(f, parents_count) + + return func + + @staticmethod + def ranking() -> SelectionFunc: + + def func(scores: array1D, parents_count: int): + return Selection.__roulette(1 + np.arange(parents_count)[::-1], parents_count) + + return func + + @staticmethod + def linear_ranking(selection_pressure: float = 1.5) -> SelectionFunc: + + assert (selection_pressure > 1 and selection_pressure < 2), f"selection_pressure should be in (1, 2), but got {selection_pressure}" + + def func(scores: array1D, parents_count: int): + tmp = parents_count * (parents_count-1) + alpha = (2 * parents_count - selection_pressure * (parents_count + 1)) / tmp + beta = 2 * (selection_pressure - 1) / tmp + + + a = -2 * alpha - beta + b = (2 * alpha + beta) ** 2 + c = 8 * beta + d = 2 * beta + + indexes = np.arange(parents_count) + + return np.array([indexes[-round((a + math.sqrt(b + c*random.random()))/d)] for _ in range(parents_count)]) + + + return func + + @staticmethod + def tournament(tau: int = 2) -> SelectionFunc: + + # NOTE + # this code really does tournament selection + # because scores are always sorted + + def func(scores: array1D, parents_count: int): + + indexes = np.arange(parents_count) + + return np.array( + [ + np.min(np.random.choice(indexes, tau, replace = False)) + for _ in range(parents_count) + ] + ) + + + return func + + + + + + + + + + + + + + + + + + + + diff --git a/geneticalgorithm2/utils.py b/geneticalgorithm2/utils.py new file mode 100644 index 0000000..dbd618a --- /dev/null +++ b/geneticalgorithm2/utils.py @@ -0,0 +1,69 @@ + +from typing import Optional, Any, Tuple + +from pathlib import Path + +import random +import numpy as np + +from .aliases import array1D, array2D + + +def fast_min(a, b): + ''' + 1.5 times faster than row min(a, b) + ''' + return a if a < b else b + + +def fast_max(a, b): + return a if a > b else b + + +def can_be_prob(value: float) -> bool: + return 0 <= value <= 1 + + +def is_current_gen_number(number: Optional[int]): + return (number is None) or (type(number) == int and number > 0) + + +def is_numpy(arg: Any): + return isinstance(arg, np.ndarray) + + +def split_matrix(mat: array2D) -> Tuple[array2D, array1D]: + """ + splits wide pop matrix to variables and scores + """ + return mat[:, :-1], mat[:, -1] + + +def union_to_matrix(variables_2D: array2D, scores_1D: array1D) -> array2D: + """ + union variables and scores to wide pop matrix + """ + return np.hstack((variables_2D, scores_1D[:, np.newaxis])) + + +def random_indexes_pair(seq_len: int) -> Tuple[int, int]: + """works 3 times faster than `random.sample(range(seq_len), 2)`""" + a = random.randrange(seq_len) + b = random.randrange(seq_len) + if a == b: + while a == b: + b = random.randrange(seq_len) + return a, b + + + + + + + + + + + + + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..4af0180 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ + +-r requirements.txt +-r requirements-extra.txt + +ipython +pytest +wheel +setuptools diff --git a/requirements-extra.txt b/requirements-extra.txt new file mode 100644 index 0000000..ab143b0 --- /dev/null +++ b/requirements-extra.txt @@ -0,0 +1,3 @@ + +joblib +func_timeout diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4088d11 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ + +matplotlib +numpy +typing_extensions +OppOpPopInit>=2.0.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0e63448 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ + +from pathlib import Path + +import setuptools + +def parse_requirements(requirements: str): + with open(requirements) as f: + return [ + l.strip('\n') for l in f if l.strip('\n') and not l.startswith('#') + ] + +with open("README.md", "r") as fh: + long_description = fh.read() + + +setuptools.setup( + name="geneticalgorithm2", + version=Path('version.txt').read_text(encoding='utf-8').strip(), + author="Demetry Pascal", + author_email="qtckpuhdsa@gmail.com", + maintainer='Demetry Pascal', + description="Supported highly optimized and flexible genetic algorithm package for python", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/PasaOpasen/geneticalgorithm2", + license='MIT', + keywords=[ + 'solve', 'solver', 'equation', + 'optimization', 'problem', 'genetic', + 'algorithm', 'GA', 'easy', 'fast', 'genetic-algorithm', + 'combinatorial', 'mixed', 'evolutionary', + ], + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + install_requires=parse_requirements('./requirements.txt'), + extras_require={ + 'full': parse_requirements('./requirements-extra.txt') + } +) + + + + + diff --git a/tests/AlgorithmParameters.py b/tests/AlgorithmParameters.py new file mode 100644 index 0000000..6fed7b6 --- /dev/null +++ b/tests/AlgorithmParameters.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Oct 16 15:22:52 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import AlgorithmParams +from geneticalgorithm2 import geneticalgorithm2 as ga + +def function(X): + return np.sum(X) + + +var_bound = np.array([[0,10]]*3) + + +model = ga(function, dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + variable_type_mixed = None, + function_timeout = 10, + algorithm_parameters={'max_num_iteration': None, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None} + ) + +model.run(no_plot = False) + + +# from version 6.3.0 it is equal to + +model = ga(function, dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + variable_type_mixed = None, + function_timeout = 10, + algorithm_parameters=AlgorithmParams( + max_num_iteration = None, + population_size = 100, + mutation_probability = 0.1, + elit_ratio = 0.01, + crossover_probability = 0.5, + parents_portion = 0.3, + crossover_type = 'uniform', + mutation_type = 'uniform_by_center', + selection_type = 'roulette', + max_iteration_without_improv = None + ) + ) + + +model.run(no_plot = False) + + +# or + +model = ga(function, dimension = 3, + variable_type='real', + variable_boundaries = var_bound, + variable_type_mixed = None, + function_timeout = 10, + algorithm_parameters=AlgorithmParams( ) + ) + + +model.run(no_plot = False) + + + + + + + + + + + + + + + + + diff --git a/tests/Standard GA vs. Elitist GA.py b/tests/Standard GA vs. Elitist GA.py new file mode 100644 index 0000000..3693ce3 --- /dev/null +++ b/tests/Standard GA vs. Elitist GA.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Nov 28 13:32:18 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import AlgorithmParams +import matplotlib.pyplot as plt + +dim = 25 + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*dim + +start_gen = np.random.uniform(0, 10, (100, dim)) + +ratios = [0, 0.02, 0.05, 0.1] + +for elit in ratios: + + model = ga(function=f, dimension=dim, variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=AlgorithmParams(max_num_iteration=400, elit_ratio=elit) + ) + + model.run(no_plot = True, start_generation=(start_gen, None), seed=1) + + plt.plot(model.report, label = f"elit_ratio = {elit}") + + + +plt.xlabel('Generation') +plt.ylabel('Minimized function') +plt.title('Standard GA vs. Elitist GA') +plt.legend() + + +plt.savefig("./output/standard_vs_elitist.png", dpi = 300) +plt.show() + + diff --git a/tests/_sense_of_prob_cross.py b/tests/_sense_of_prob_cross.py new file mode 100644 index 0000000..eafbf0a --- /dev/null +++ b/tests/_sense_of_prob_cross.py @@ -0,0 +1,63 @@ + + + +import sys +sys.path.append('..') + +import numpy as np + +from OppOpPopInit import set_seed +from geneticalgorithm2 import geneticalgorithm2 as ga, AlgorithmParams, plot_several_lines + +set_seed(1) + +def f(X): + return np.sum(X) + np.abs(X).sum() - X.mean() + +varbound = ( + (0.5, 1.5), + (1, 100), + (0, 1), + (10, 13), + (-10, 10), + (-50, 10) +) + +vartype = ('real', 'int', 'real', 'real', 'real', 'int') + + +reports = [] +probs = [0.1, 0.3, 0.5, 0.8, 1] + +for p in probs: + + arrs = [] + + for i in range(15): + model = ga( + function=f, dimension=len(vartype), + variable_type=vartype, + variable_boundaries=varbound, + algorithm_parameters=AlgorithmParams( + crossover_probability=p, + max_num_iteration=400, + elit_ratio=0.01 + ) + ) + + model.run(no_plot=True) + arrs.append(model.report) + + reports.append(np.array(arrs).mean(axis=0)) + +plot_several_lines( + reports, + colors=['red', 'blue', 'green', 'black', 'yellow', 'orange'], + labels=[f"prob = {v}" for v in probs], + save_as='./output/sense_of_crossover_prob__no_sense.png', + + title='result of different crossover probs', + ylabel='avg score' +) + + diff --git a/tests/best_of_N.py b/tests/best_of_N.py new file mode 100644 index 0000000..7aea48b --- /dev/null +++ b/tests/best_of_N.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Dec 7 16:14:37 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Population_initializer + + +def f(X): + return 10*np.sum(X) - X[0] - X[1] - X[2]*X[3] + X[6]**2 + 1/(0.01 + X[5]) + +dim = 15 +iterations = 150 + +varbound = np.array([[0,10]]*dim) + +model = ga(function=f, dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': iterations, + 'population_size': 400 + }) + + +for best_of in (1, 3, 5): + + average_report = np.zeros(iterations+1) + + for _ in range(40): + model.run(no_plot = True, + population_initializer=Population_initializer(select_best_of = best_of) + ) + average_report += np.array(model.report) + + average_report /= 40 + + plt.plot(average_report, label = f"selected best N from {best_of}N") + + +plt.xlabel('Generation') +plt.ylabel('Minimized function (40 simulations average)') +plt.title('Selection best N objects before running GA') +plt.legend() + + +plt.savefig("./output/init_best_of.png", dpi = 300) +plt.show() \ No newline at end of file diff --git a/tests/best_of_N_with_opp.py b/tests/best_of_N_with_opp.py new file mode 100644 index 0000000..54a7585 --- /dev/null +++ b/tests/best_of_N_with_opp.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Dec 18 19:40:38 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from OppOpPopInit import OppositionOperators +from OptimizationTestFunctions import Ackley + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Population_initializer + + +dim = 15 + +func = Ackley(dim = dim) + +iterations = 150 + +varbound = np.array([[-4,3]]*dim) + +model = ga(function=func, dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': iterations, + 'population_size': 400 + }) + + +oppositors = [ + None, + + [ + OppositionOperators.Continual.quasi(minimums = varbound[:,0], maximums = varbound[:, 1]) + ], + + [ + OppositionOperators.Continual.quasi(minimums = varbound[:,0], maximums = varbound[:, 1]), + OppositionOperators.Continual.over(minimums = varbound[:,0], maximums = varbound[:, 1]) + ] +] + +names = [ + 'No oppositor, just random', + 'quasi oppositor', + 'quasi + over oppositors' + ] + + +for opp, name in zip(oppositors, names): + + average_report = np.zeros(iterations) + + for _ in range(40): + model.run(no_plot = True, + population_initializer=Population_initializer(select_best_of = 3), + init_oppositors=opp + ) + average_report += np.array(model.report) + + average_report /= 40 + + plt.plot(average_report, label = name) + + +plt.xlabel('Generation') +plt.ylabel('Minimized function (40 simulations average)') +plt.title('Start gen. using oppositors') +plt.legend() + + +plt.savefig("./output/init_best_of_opp.png", dpi = 300) +plt.show() \ No newline at end of file diff --git a/tests/best_subset.py b/tests/best_subset.py new file mode 100644 index 0000000..4062376 --- /dev/null +++ b/tests/best_subset.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Nov 28 21:09:48 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + + +subset_size = 20 # how many objects we can choose + +objects_count = 100 # how many objects are in set + +my_set = np.random.random(objects_count)*10 - 5 # set values + +# minimized function +def f(X): + return abs(np.mean(my_set[X==1]) - np.median(my_set[X==1])) + +# initialize start generation and params + +N = 1000 # size of population +start_generation = np.zeros((N, objects_count)) +indexes = np.arange(0, objects_count, dtype = np.int8) # indexes of variables + +for i in range(N): + inds = np.random.choice(indexes, subset_size, replace = False) + start_generation[i, inds] = 1 + + +def my_crossover(parent_a, parent_b): + a_indexes = set(indexes[parent_a == 1]) + b_indexes = set(indexes[parent_b == 1]) + + intersect = a_indexes.intersection(b_indexes) # elements in both parents + a_only = a_indexes - intersect # elements only in 'a' parent + b_only = b_indexes - intersect + + child_inds = np.array(list(a_only) + list(b_only), dtype = np.int8) + np.random.shuffle(child_inds) # mix + + childs = np.zeros((2, parent_a.size)) + if intersect: + childs[:, np.array(list(intersect))] = 1 + childs[0, child_inds[:int(child_inds.size/2)]] = 1 + childs[1, child_inds[int(child_inds.size/2):]] = 1 + + return childs[0,:], childs[1,:] + + +model = ga(function=f, + dimension=objects_count, + variable_type='bool', + algorithm_parameters={ + 'max_num_iteration': 500, + 'mutation_probability': 0, # no mutation, just crossover + 'elit_ratio': 0.05, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type': my_crossover, + 'max_iteration_without_improv': 20 + } + ) + +model.run(no_plot = False, start_generation=(start_generation, None)) \ No newline at end of file diff --git a/tests/callbacks.py b/tests/callbacks.py new file mode 100644 index 0000000..2e151c1 --- /dev/null +++ b/tests/callbacks.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 31 19:54:11 2020 + +@author: qtckp +""" +import sys +sys.path.append('..') + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Callbacks, MiddleCallbacks, ActionConditions + + +dim = 16 + +def f(X): + pen=0 + if np.sum(X) < 1: + pen=500+10*(1-np.sum(X)) + return np.sum(X)+pen + +varbound=np.array([[0,10]]*dim) + +model=ga(function=f, + dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': 2000 + }) + +model.run( + callbacks=[ + Callbacks.SavePopulation('./output/callback/pop_example', save_gen_step=500, file_prefix='constraints'), + Callbacks.PlotOptimizationProcess('./output/callback/plot_example', save_gen_step=300, show = False, main_color='red', file_prefix='plot') + ] +) + + +# doing nothing, just for test +model.run( + middle_callbacks=[ + MiddleCallbacks.UniversalCallback(lambda data: data, ActionConditions.EachGen(generation_step=1)) + ] +) \ No newline at end of file diff --git a/tests/check_new_types_of_cross_and_mut.py b/tests/check_new_types_of_cross_and_mut.py new file mode 100644 index 0000000..58cdf6c --- /dev/null +++ b/tests/check_new_types_of_cross_and_mut.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 27 22:02:24 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Crossover, Mutations + +def f(X): + return np.sum(X) + +varbound = np.array([[0,10]]*3) + + +mutations = ( + Mutations.gauss_by_center(0.2), + Mutations.gauss_by_center(0.4), + Mutations.gauss_by_x(), + Mutations.gauss_by_x(0.2), + Mutations.uniform_by_center(), + Mutations.uniform_by_x(), + 'uniform_by_x', + 'uniform_by_center', + 'gauss_by_x', + 'gauss_by_center' + ) + +crossovers = ( + Crossover.one_point(), + 'one_point', + Crossover.two_point(), + 'two_point', + Crossover.uniform(), + 'uniform', + Crossover.shuffle(), + 'shuffle', + Crossover.segment(), + 'segment', + # only for real!!!! + Crossover.mixed(), + Crossover.arithmetic() + ) + + +for mutation in mutations: + for crossover in crossovers: + print(f"mutation = {mutation}, crossover = {crossover}") + + + algorithm_param = { + 'max_num_iteration': 400, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':crossover, + 'mutation_type': mutation, + 'max_iteration_without_improv':None + } + + model = ga(function=f, dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = algorithm_param) + + + + model.run(no_plot = False) \ No newline at end of file diff --git a/tests/constraints.py b/tests/constraints.py new file mode 100644 index 0000000..64562a5 --- /dev/null +++ b/tests/constraints.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 19 16:22:26 2020 + +@author: qtckp +""" + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + pen=0 + if X[0]+X[1]<2: + pen=500+1000*(2-X[0]-X[1]) + return np.sum(X)+pen + +varbound=np.array([[0,10]]*3) + +model=ga(function=f,dimension=3,variable_type='real',variable_boundaries=varbound) + +model.run() \ No newline at end of file diff --git a/tests/crossovers_examples.py b/tests/crossovers_examples.py new file mode 100644 index 0000000..dd8383e --- /dev/null +++ b/tests/crossovers_examples.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 25 12:40:24 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import Crossover + + +crossovers = [ + Crossover.one_point(), + Crossover.two_point(), + Crossover.uniform(), + Crossover.uniform_window(window = 3), + Crossover.shuffle(), + Crossover.segment(), + + Crossover.arithmetic(), + Crossover.mixed(alpha = 0.4) + ] + + + +x = np.ones(15) +y = x*0 + +lines = [] +for cr in crossovers: + + new_x, new_y = cr(x, y) + + print(cr.__qualname__.split('.')[1]) + print(new_x) + print(new_y) + print() + + lines += [ + f"* **{cr.__qualname__.split('.')[1]}**:\n", + '|' + ' | '.join(np.round(new_x, 2).astype(str))+'|', + '|' + ' | '.join( [':---:']*x.size )+'|', + '|' + ' | '.join(np.round(new_y, 2).astype(str))+'|', + '' + ] + + +with open('./output/crossovers_example.txt', 'w') as file: + file.writelines([line.replace('.0','') + '\n' for line in lines]) + + + + + + + diff --git a/tests/disable_messages_and_printing.py b/tests/disable_messages_and_printing.py new file mode 100644 index 0000000..5cf33c8 --- /dev/null +++ b/tests/disable_messages_and_printing.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue May 18 11:46:10 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,30]]*20 + +model = ga(function=f, dimension=20, variable_type='real', variable_boundaries=varbound) + +result = model.run( + no_plot = True, + progress_bar_stream=None, + disable_printing=True +) + +print(result.function) \ No newline at end of file diff --git a/tests/init_local_opt.py b/tests/init_local_opt.py new file mode 100644 index 0000000..a12c3a8 --- /dev/null +++ b/tests/init_local_opt.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Dec 7 19:14:48 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from DiscreteHillClimbing import Hill_Climbing_descent + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Population_initializer + + +def f(arr): + arr2 = arr/25 + return -np.sum(arr2*np.sin(np.sqrt(np.abs(arr2))))**5 + np.sum(np.abs(arr2))**2 + +iterations = 100 + +varbound = np.array([[-100, 100]]*15) + +available_values = [np.arange(-100, 101)]*15 + + +my_local_optimizer = lambda arr, score: Hill_Climbing_descent(function = f, available_predictors_values=available_values, max_function_evals=50, start_solution=arr ) + + +model = ga(function=f, dimension=varbound.shape[0], + variable_type='int', + variable_boundaries = varbound, + algorithm_parameters={ + 'max_num_iteration': iterations, + 'population_size': 400 + }) + + +for time in ('before_select', 'after_select', 'never'): + + + model.run(no_plot = True, + population_initializer = Population_initializer( + select_best_of = 3, + local_optimization_step = time, + local_optimizer = my_local_optimizer + ) + ) + + + plt.plot(model.report, label = f"local optimization time = '{time}'") + + +plt.xlabel('Generation') +plt.ylabel('Minimized function (40 simulations average)') +plt.title('Selection best N object before running GA') +plt.legend() + +plt.savefig("./output/init_local_opt.png", dpi = 300) +plt.show() + diff --git a/tests/min of sum int.py b/tests/min of sum int.py new file mode 100644 index 0000000..7727fa4 --- /dev/null +++ b/tests/min of sum int.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 19 16:18:37 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*3 + +model = ga(function=f, dimension=3, variable_type='int', variable_boundaries=varbound) + +model.run() + + +# check discrete mutation + +varbound = [[0,10]]*300 + +model = ga(function=f, dimension=300, variable_type='int', + variable_boundaries=varbound, + algorithm_parameters={ + 'mutation_discrete_type': 'uniform_discrete', + 'max_num_iteration': 1000 + }) + +model.run(stop_when_reached=0) + + +model = ga(function=f, dimension=300, variable_type='int', + variable_boundaries=varbound, + algorithm_parameters={ + 'mutation_discrete_type': lambda x, left, right: left, + 'max_num_iteration': 1000 + }) + +model.run(stop_when_reached=0) + + diff --git a/tests/min of sum mixed.py b/tests/min of sum mixed.py new file mode 100644 index 0000000..4710ace --- /dev/null +++ b/tests/min of sum mixed.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 19 16:20:05 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +varbound = ( + (0.5, 1.5), + (1, 100), + (0, 1) +) + +vartype = ('real', 'int', 'int') + +model = ga( + function=f, dimension=len(vartype), + variable_type=vartype, + variable_boundaries=varbound +) + +model.run() + + diff --git a/tests/min of sum.py b/tests/min of sum.py new file mode 100644 index 0000000..33fead4 --- /dev/null +++ b/tests/min of sum.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 19 16:15:47 2020 + +@author: qtckp +""" +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*20 + +model = ga(function=f, dimension=20, variable_type='real', variable_boundaries=varbound) + +model.run(no_plot = False) \ No newline at end of file diff --git a/tests/multiprocess.py b/tests/multiprocess.py new file mode 100644 index 0000000..bd34a82 --- /dev/null +++ b/tests/multiprocess.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Nov 21 20:16:57 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + import math + a = X[0] + b = X[1] + c = X[2] + s = 0 + for i in range(10000): + s += math.sin(a*i) + math.sin(b*i) + math.cos(c*i) + + return s + + +algorithm_param = {'max_num_iteration': 50, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'max_iteration_without_improv':None} + +varbound = np.array([[-10,10]]*3) + +model = ga(function=f, dimension=3, variable_type='real', variable_boundaries=varbound, algorithm_parameters = algorithm_param) + +#%time model.run(no_plot = False) +#%time model.run(no_plot = False, set_function= ga.set_function_multiprocess(f, n_jobs = 6)) \ No newline at end of file diff --git a/tests/mut_indexes.py b/tests/mut_indexes.py new file mode 100644 index 0000000..78b38c6 --- /dev/null +++ b/tests/mut_indexes.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Created on Sun Jan 31 20:18:39 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import AlgorithmParams + +def f(X): + return np.sum(X) + +dim = 7 + +varbound = [(0,10)]*dim + +model = ga( + function = f, + dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=AlgorithmParams( + max_num_iteration=None, + mutation_probability=0.2, + elit_ratio=0.01, + crossover_probability=0.5, + parents_portion=0.3, + crossover_type='uniform', + mutation_type='uniform_by_center', + selection_type='roulette', + max_iteration_without_improv=None + ) +) + +pop = np.full((10, dim), 5, dtype = np.float32) + +model.run(no_plot = False, + start_generation = (pop, None) + ) + +print(model.result.last_generation.variables) + + +# freeze dims [0,1,2,3] for mutation -- they won't be changed +model.run(no_plot = False, + start_generation = (pop, None), + mutation_indexes= [6, 5, 4] + ) + + +print(model.result.last_generation.variables) + diff --git a/tests/optimization_test_functions.py b/tests/optimization_test_functions.py new file mode 100644 index 0000000..3318d2a --- /dev/null +++ b/tests/optimization_test_functions.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 19 16:25:17 2020 + +@author: qtckp +""" +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +from OptimizationTestFunctions import Sphere, Ackley, AckleyTest, Rosenbrock, Fletcher, Griewank, Penalty2, Quartic, Rastrigin, SchwefelDouble, SchwefelMax, SchwefelAbs, SchwefelSin, Stairs, Abs, Michalewicz, Scheffer, Eggholder, Weierstrass + + +dim = 2 + +functions = [ + Sphere(dim, degree = 2), + Ackley(dim), + AckleyTest(dim), + Rosenbrock(dim), + Fletcher(dim, seed = 1488), + Griewank(dim), + Penalty2(dim), + Quartic(dim), + Rastrigin(dim), + SchwefelDouble(dim), + SchwefelMax(dim), + SchwefelAbs(dim), + SchwefelSin(dim), + Stairs(dim), + Abs(dim), + Michalewicz(), + Scheffer(dim), + Eggholder(dim), + Weierstrass(dim) +] + + + + +for f in functions: + + xmin, xmax, ymin, ymax = f.bounds + + varbound = np.array([[xmin, xmax], [ymin, ymax]]) + + model = ga(function=f, + dimension = dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = { + 'max_num_iteration': 500, + 'population_size': 100, + 'mutation_probability': 0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv':100 + } + ) + + model.run(no_plot = True, + stop_when_reached = (f.f_best + 1e-5/(xmax - xmin)) if f.f_best is not None else None + ) + + title = f"Optimization process for {type(f).__name__}" + + model.plot_results( + title = title, + save_as = f"./output/opt_test_funcs/{title}.png", + main_color = 'green' + ) diff --git a/tests/output/1212.0.png b/tests/output/1212.0.png new file mode 100644 index 0000000..bc2f6c3 Binary files /dev/null and b/tests/output/1212.0.png differ diff --git a/tests/output/1258.0.png b/tests/output/1258.0.png new file mode 100644 index 0000000..fc97714 Binary files /dev/null and b/tests/output/1258.0.png differ diff --git a/tests/output/1321.0.png b/tests/output/1321.0.png new file mode 100644 index 0000000..5b5e4cb Binary files /dev/null and b/tests/output/1321.0.png differ diff --git a/tests/output/1350.0.png b/tests/output/1350.0.png new file mode 100644 index 0000000..3dd9c56 Binary files /dev/null and b/tests/output/1350.0.png differ diff --git a/tests/output/1389.0.png b/tests/output/1389.0.png new file mode 100644 index 0000000..ebb8091 Binary files /dev/null and b/tests/output/1389.0.png differ diff --git a/tests/output/1394.0.png b/tests/output/1394.0.png new file mode 100644 index 0000000..81567ec Binary files /dev/null and b/tests/output/1394.0.png differ diff --git a/tests/output/1507.0.png b/tests/output/1507.0.png new file mode 100644 index 0000000..51a5c8c Binary files /dev/null and b/tests/output/1507.0.png differ diff --git a/tests/output/1742.0.png b/tests/output/1742.0.png new file mode 100644 index 0000000..bdfa092 Binary files /dev/null and b/tests/output/1742.0.png differ diff --git a/tests/output/callback/plot_example/plot_1200.png b/tests/output/callback/plot_example/plot_1200.png new file mode 100644 index 0000000..426407c Binary files /dev/null and b/tests/output/callback/plot_example/plot_1200.png differ diff --git a/tests/output/callback/plot_example/plot_1500.png b/tests/output/callback/plot_example/plot_1500.png new file mode 100644 index 0000000..acbfe36 Binary files /dev/null and b/tests/output/callback/plot_example/plot_1500.png differ diff --git a/tests/output/callback/plot_example/plot_1800.png b/tests/output/callback/plot_example/plot_1800.png new file mode 100644 index 0000000..059faee Binary files /dev/null and b/tests/output/callback/plot_example/plot_1800.png differ diff --git a/tests/output/callback/plot_example/plot_300.png b/tests/output/callback/plot_example/plot_300.png new file mode 100644 index 0000000..2884f86 Binary files /dev/null and b/tests/output/callback/plot_example/plot_300.png differ diff --git a/tests/output/callback/plot_example/plot_600.png b/tests/output/callback/plot_example/plot_600.png new file mode 100644 index 0000000..3121819 Binary files /dev/null and b/tests/output/callback/plot_example/plot_600.png differ diff --git a/tests/output/callback/plot_example/plot_900.png b/tests/output/callback/plot_example/plot_900.png new file mode 100644 index 0000000..6c35d42 Binary files /dev/null and b/tests/output/callback/plot_example/plot_900.png differ diff --git a/tests/output/callback/pop_example/constraints_1000.npz b/tests/output/callback/pop_example/constraints_1000.npz new file mode 100644 index 0000000..ee21d81 Binary files /dev/null and b/tests/output/callback/pop_example/constraints_1000.npz differ diff --git a/tests/output/callback/pop_example/constraints_1500.npz b/tests/output/callback/pop_example/constraints_1500.npz new file mode 100644 index 0000000..cfcf16b Binary files /dev/null and b/tests/output/callback/pop_example/constraints_1500.npz differ diff --git a/tests/output/callback/pop_example/constraints_2000.npz b/tests/output/callback/pop_example/constraints_2000.npz new file mode 100644 index 0000000..f6688fe Binary files /dev/null and b/tests/output/callback/pop_example/constraints_2000.npz differ diff --git a/tests/output/callback/pop_example/constraints_500.npz b/tests/output/callback/pop_example/constraints_500.npz new file mode 100644 index 0000000..405b04e Binary files /dev/null and b/tests/output/callback/pop_example/constraints_500.npz differ diff --git a/tests/output/crossovers_example.txt b/tests/output/crossovers_example.txt new file mode 100644 index 0000000..ba49262 --- /dev/null +++ b/tests/output/crossovers_example.txt @@ -0,0 +1,48 @@ +* **one_point**: + +|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0| + +* **two_point**: + +|1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0| + +* **uniform**: + +|0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1| + +* **uniform_window**: + +|1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1| + +* **shuffle**: + +|1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0| + +* **segment**: + +|0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0| + +* **arithmetic**: + +|0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75 | 0.75| + +* **mixed**: + +|1.33 | 1.18 | -0.13 | 0.84 | 0.1 | 0.5 | 14 | 0.19 | 0.68 | 0.2 | -0.11 | 0.51 | 0.71 | 1.23 | 0.6| +|:---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---:| +|0.48 | 0.43 | 0.5 | 0.59 | 0.59 | 0.44 | 0.42 | 0.59 | 0.49 | 0.54 | 0.46 | 0.6 | 0.57 | 0.52 | 0.4| + diff --git a/tests/output/eggholder_lastgen.npz b/tests/output/eggholder_lastgen.npz new file mode 100644 index 0000000..dc570b2 Binary files /dev/null and b/tests/output/eggholder_lastgen.npz differ diff --git a/tests/output/init_best_of.png b/tests/output/init_best_of.png new file mode 100644 index 0000000..d43af12 Binary files /dev/null and b/tests/output/init_best_of.png differ diff --git a/tests/output/init_best_of_opp.png b/tests/output/init_best_of_opp.png new file mode 100644 index 0000000..c90e5ca Binary files /dev/null and b/tests/output/init_best_of_opp.png differ diff --git a/tests/output/init_local_opt.png b/tests/output/init_local_opt.png new file mode 100644 index 0000000..209b979 Binary files /dev/null and b/tests/output/init_local_opt.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Abs.png b/tests/output/opt_test_funcs/Optimization process for Abs.png new file mode 100644 index 0000000..085d8ea Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Abs.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Ackley.png b/tests/output/opt_test_funcs/Optimization process for Ackley.png new file mode 100644 index 0000000..35a19bc Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Ackley.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for AckleyTest.png b/tests/output/opt_test_funcs/Optimization process for AckleyTest.png new file mode 100644 index 0000000..662fc59 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for AckleyTest.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Eggholder.png b/tests/output/opt_test_funcs/Optimization process for Eggholder.png new file mode 100644 index 0000000..9e920e9 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Eggholder.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Fletcher.png b/tests/output/opt_test_funcs/Optimization process for Fletcher.png new file mode 100644 index 0000000..d04408c Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Fletcher.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Griewank.png b/tests/output/opt_test_funcs/Optimization process for Griewank.png new file mode 100644 index 0000000..f38ae84 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Griewank.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Michalewicz.png b/tests/output/opt_test_funcs/Optimization process for Michalewicz.png new file mode 100644 index 0000000..1ab679d Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Michalewicz.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Penalty2.png b/tests/output/opt_test_funcs/Optimization process for Penalty2.png new file mode 100644 index 0000000..dc4075e Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Penalty2.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Quartic.png b/tests/output/opt_test_funcs/Optimization process for Quartic.png new file mode 100644 index 0000000..e87c214 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Quartic.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Rastrigin.png b/tests/output/opt_test_funcs/Optimization process for Rastrigin.png new file mode 100644 index 0000000..9f7f3a6 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Rastrigin.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Rosenbrock.png b/tests/output/opt_test_funcs/Optimization process for Rosenbrock.png new file mode 100644 index 0000000..bcb72fa Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Rosenbrock.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Scheffer.png b/tests/output/opt_test_funcs/Optimization process for Scheffer.png new file mode 100644 index 0000000..8fcd509 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Scheffer.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for SchwefelAbs.png b/tests/output/opt_test_funcs/Optimization process for SchwefelAbs.png new file mode 100644 index 0000000..7b4715c Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for SchwefelAbs.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for SchwefelDouble.png b/tests/output/opt_test_funcs/Optimization process for SchwefelDouble.png new file mode 100644 index 0000000..35ba669 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for SchwefelDouble.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for SchwefelMax.png b/tests/output/opt_test_funcs/Optimization process for SchwefelMax.png new file mode 100644 index 0000000..6930e99 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for SchwefelMax.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for SchwefelSin.png b/tests/output/opt_test_funcs/Optimization process for SchwefelSin.png new file mode 100644 index 0000000..6819813 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for SchwefelSin.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Sphere.png b/tests/output/opt_test_funcs/Optimization process for Sphere.png new file mode 100644 index 0000000..27ee0b3 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Sphere.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Stairs.png b/tests/output/opt_test_funcs/Optimization process for Stairs.png new file mode 100644 index 0000000..c119925 Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Stairs.png differ diff --git a/tests/output/opt_test_funcs/Optimization process for Weierstrass.png b/tests/output/opt_test_funcs/Optimization process for Weierstrass.png new file mode 100644 index 0000000..657d5af Binary files /dev/null and b/tests/output/opt_test_funcs/Optimization process for Weierstrass.png differ diff --git a/tests/output/opt_test_funcs/optimization_test_func_code_for_md.txt b/tests/output/opt_test_funcs/optimization_test_func_code_for_md.txt new file mode 100644 index 0000000..29f3f4d --- /dev/null +++ b/tests/output/opt_test_funcs/optimization_test_func_code_for_md.txt @@ -0,0 +1,76 @@ +### [Sphere](https://github.com/PasaOpasen/OptimizationTestFunctions#sphere) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Sphere.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Sphere.png) + +### [Ackley](https://github.com/PasaOpasen/OptimizationTestFunctions#ackley) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Ackley.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Ackley.png) + +### [AckleyTest](https://github.com/PasaOpasen/OptimizationTestFunctions#ackleytest) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20AckleyTest.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20AckleyTest.png) + +### [Rosenbrock](https://github.com/PasaOpasen/OptimizationTestFunctions#rosenbrock) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Rosenbrock.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Rosenbrock.png) + +### [Fletcher](https://github.com/PasaOpasen/OptimizationTestFunctions#fletcher) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Fletcher.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Fletcher.png) + +### [Griewank](https://github.com/PasaOpasen/OptimizationTestFunctions#griewank) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Griewank.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Griewank.png) + +### [Penalty2](https://github.com/PasaOpasen/OptimizationTestFunctions#penalty2) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Penalty2.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Penalty2.png) + +### [Quartic](https://github.com/PasaOpasen/OptimizationTestFunctions#quartic) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Quartic.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Quartic.png) + +### [Rastrigin](https://github.com/PasaOpasen/OptimizationTestFunctions#rastrigin) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Rastrigin.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Rastrigin.png) + +### [SchwefelDouble](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefeldouble) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelDouble.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelDouble.png) + +### [SchwefelMax](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelmax) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelMax.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelMax.png) + +### [SchwefelAbs](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelabs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelAbs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelAbs.png) + +### [SchwefelSin](https://github.com/PasaOpasen/OptimizationTestFunctions#schwefelsin) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20SchwefelSin.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20SchwefelSin.png) + +### [Stairs](https://github.com/PasaOpasen/OptimizationTestFunctions#stairs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Stairs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Stairs.png) + +### [Abs](https://github.com/PasaOpasen/OptimizationTestFunctions#abs) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Abs.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Abs.png) + +### [Michalewicz](https://github.com/PasaOpasen/OptimizationTestFunctions#michalewicz) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Michalewicz.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Michalewicz.png) + +### [Scheffer](https://github.com/PasaOpasen/OptimizationTestFunctions#scheffer) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Scheffer.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Scheffer.png) + +### [Eggholder](https://github.com/PasaOpasen/OptimizationTestFunctions#eggholder) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Eggholder.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Eggholder.png) + +### [Weierstrass](https://github.com/PasaOpasen/OptimizationTestFunctions#weierstrass) +![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20Weierstrass.png) +![](tests/output/opt_test_funcs/Optimization%20process%20for%20Weierstrass.png) + diff --git a/tests/output/plot_scores_end.png b/tests/output/plot_scores_end.png new file mode 100644 index 0000000..a2c444d Binary files /dev/null and b/tests/output/plot_scores_end.png differ diff --git a/tests/output/plot_scores_process.png b/tests/output/plot_scores_process.png new file mode 100644 index 0000000..ce47daf Binary files /dev/null and b/tests/output/plot_scores_process.png differ diff --git a/tests/output/plot_scores_start.png b/tests/output/plot_scores_start.png new file mode 100644 index 0000000..606ba6b Binary files /dev/null and b/tests/output/plot_scores_start.png differ diff --git a/tests/output/remove_dups.png b/tests/output/remove_dups.png new file mode 100644 index 0000000..4eadfd4 Binary files /dev/null and b/tests/output/remove_dups.png differ diff --git a/tests/output/report.png b/tests/output/report.png new file mode 100644 index 0000000..4826f6d Binary files /dev/null and b/tests/output/report.png differ diff --git a/tests/output/revolution.png b/tests/output/revolution.png new file mode 100644 index 0000000..7a1cb21 Binary files /dev/null and b/tests/output/revolution.png differ diff --git a/tests/output/selections.png b/tests/output/selections.png new file mode 100644 index 0000000..f18d62f Binary files /dev/null and b/tests/output/selections.png differ diff --git a/tests/output/sense_of_crossover_prob__no_sense.png b/tests/output/sense_of_crossover_prob__no_sense.png new file mode 100644 index 0000000..d50c6c3 Binary files /dev/null and b/tests/output/sense_of_crossover_prob__no_sense.png differ diff --git a/tests/output/standard_vs_elitist.png b/tests/output/standard_vs_elitist.png new file mode 100644 index 0000000..b660a45 Binary files /dev/null and b/tests/output/standard_vs_elitist.png differ diff --git a/tests/output/studEA.png b/tests/output/studEA.png new file mode 100644 index 0000000..3c05754 Binary files /dev/null and b/tests/output/studEA.png differ diff --git a/tests/output/with_dups.png b/tests/output/with_dups.png new file mode 100644 index 0000000..96eeb92 Binary files /dev/null and b/tests/output/with_dups.png differ diff --git a/tests/output/without_dups.png b/tests/output/without_dups.png new file mode 100644 index 0000000..715e7c0 Binary files /dev/null and b/tests/output/without_dups.png differ diff --git a/tests/plot_diversities.py b/tests/plot_diversities.py new file mode 100644 index 0000000..cf96ce4 --- /dev/null +++ b/tests/plot_diversities.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jul 3 20:58:08 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +from geneticalgorithm2 import MiddleCallbacks + + +dim = 6 +rd = np.random.random(size = dim) + + +def f(X): + return np.mean(X - rd) + + +varbound = np.array([[0, 1]]*dim) + +model = ga(function=f, dimension = dim, + variable_type='real', variable_boundaries=varbound, + + algorithm_parameters = { + 'max_num_iteration': 1000, + 'population_size':50, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv':None + } + ) + +model.run( + no_plot = False, + middle_callbacks = [ + MiddleCallbacks.GeneDiversityStats(20) + ] +) + + + diff --git a/tests/plot_each_gen_scores.py b/tests/plot_each_gen_scores.py new file mode 100644 index 0000000..cba68b0 --- /dev/null +++ b/tests/plot_each_gen_scores.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jun 4 16:17:30 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +from geneticalgorithm2 import Actions, ActionConditions, MiddleCallbacks + + +def f(X): + return np.sum(np.abs(X-50)) + + + +dim = 100 + +varbound = np.array([[0,70]]*dim) + +model = ga(function=f, + dimension=dim, + variable_type='int', + variable_boundaries=varbound + ) + +model.run( + middle_callbacks = [ + MiddleCallbacks.UniversalCallback(Actions.PlotPopulationScores( + title_pattern=lambda data: f"Gen {data['current_generation']}", + save_as_name_pattern=lambda data: f"./output/{data['last_generation']['scores'].min()}.png" + ), + ActionConditions.EachGen(1) + ) + ] +) \ No newline at end of file diff --git a/tests/plot_scores.py b/tests/plot_scores.py new file mode 100644 index 0000000..42887e8 --- /dev/null +++ b/tests/plot_scores.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Created on Sun Jan 3 14:13:11 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga + +from geneticalgorithm2 import plot_pop_scores # for plotting scores without ga object + + +def f(X): + return 50 * np.sum(X) - np.sum(np.sqrt(X) * np.sin(X)) + + +dim = 25 +varbound = np.array([[0, 10]] * dim) + + +# create start population +start_pop = np.random.uniform(0, 10, (50, dim)) +# eval scores of start population +start_scores = np.array([f(start_pop[i]) for i in range(start_pop.shape[0])]) + +# plot start scores using plot_pop_scores function +plot_pop_scores(start_scores, title = 'Population scores before beggining of searching', save_as= './output/plot_scores_start.png') + + +model = ga(function=f, dimension=dim, variable_type='real', variable_boundaries=varbound) +# run optimization process +model.run(no_plot = True, + start_generation={ + 'variables': start_pop, + 'scores': start_scores + }) +# plot and save optimization process plot +model.plot_results(save_as = './output/plot_scores_process.png') + +# plot scores of last population +model.plot_generation_scores(title = 'Population scores after ending of searching', save_as= './output/plot_scores_end.png') \ No newline at end of file diff --git a/tests/progress_bar_streams.py b/tests/progress_bar_streams.py new file mode 100644 index 0000000..e75b6a1 --- /dev/null +++ b/tests/progress_bar_streams.py @@ -0,0 +1,31 @@ + +import sys +sys.path.append('..') + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +varbound = ( + (0.5, 1.5), + (1, 100), + (-100, 1) +) + +vartype = ('real', 'real', 'int') + +model = ga( + function=f, dimension=len(vartype), + variable_type=vartype, + variable_boundaries=varbound +) + +# old!! +model.run(disable_progress_bar=True) + + +model.run(progress_bar_stream=None) +model.run(progress_bar_stream='stdout') +model.run(progress_bar_stream='stderr') diff --git a/tests/remove_dups.py b/tests/remove_dups.py new file mode 100644 index 0000000..c011bdb --- /dev/null +++ b/tests/remove_dups.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Dec 18 21:15:55 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from OppOpPopInit import OppositionOperators + +from geneticalgorithm2 import geneticalgorithm2 as ga + + + +dim = 15 +np.random.seed(3) + +rands = np.random.uniform(-10, 10, 100) + +def func(X): + return np.sum(rands[X.astype(int)]) + X.sum() + +iterations = 900 + +varbound = np.array([[0,99]]*dim) + +model = ga(function=func, dimension=dim, + variable_type='int', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': iterations + }) + + +start_pop = np.random.randint(0, 10, size = (100, dim)) + +start_gen = (start_pop, None) + + +np.random.seed(3) +model.run(no_plot = True, start_generation=start_gen) +plt.plot(model.report, label = 'without dups removing') + +np.random.seed(3) +model.run(no_plot = True, + start_generation=start_gen, + remove_duplicates_generation_step = 40, + ) + +plt.plot(model.report, label = 'with dups removing + random replace') + +np.random.seed(3) +model.run(no_plot = True, + start_generation=start_gen, + remove_duplicates_generation_step = 40, + duplicates_oppositor=OppositionOperators.Discrete.integers_by_order(minimums = varbound[:,0], maximums = varbound[:, 1]) + ) + +plt.plot(model.report, label = 'with dups removing + opposion replace') + + + + +plt.xlabel('Generation') +plt.ylabel('Minimized function') +plt.title('Duplicates removing') +plt.legend() + + +plt.savefig("./output/remove_dups.png", dpi = 300) +plt.show() \ No newline at end of file diff --git a/tests/remove_dups_by_callback.py b/tests/remove_dups_by_callback.py new file mode 100644 index 0000000..938dc2d --- /dev/null +++ b/tests/remove_dups_by_callback.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jan 30 14:15:51 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +from geneticalgorithm2 import ActionConditions, Actions, MiddleCallbacks + +from OppOpPopInit import OppositionOperators, SampleInitializers + +np.random.seed(1) + +def converter(arr): + arrc = np.full_like(arr, 3) + arrc[arr < 0.75 ] = 2 + arrc[arr < 0.50 ] = 1 + arrc[arr < 0.25 ] = 0 + return arrc + +def f(X): + return np.sum(converter(X)) + +dim = 60 + +varbound = np.array([[0, 1]]*dim) +start_pop = SampleInitializers.CreateSamples( + SampleInitializers.Uniform(minimums=varbound[:, 0], maximums=varbound[:, 1]), + count=50 +) + +model = ga(function=f, dimension = dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = { + 'max_num_iteration': 1000, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv': None + } +) + +model.run( + no_plot = False, + stop_when_reached = 0, + start_generation=(start_pop, None) +) +model.plot_generation_scores(title = 'Population scores after ending of searching', save_as= './output/with_dups.png') + +model.run( + no_plot = False, + stop_when_reached = 0, + start_generation=(start_pop, None), + middle_callbacks = [ + MiddleCallbacks.UniversalCallback( + Actions.RemoveDuplicates( + oppositor = OppositionOperators.Continual.abs( + minimums = varbound[:, 0], + maximums = varbound[:, 1] + ), + converter = converter + ), + ActionConditions.EachGen(15)) + ] + +) + +model.plot_generation_scores(title = 'Population scores after ending of searching', save_as= './output/without_dups.png') + + + + + diff --git a/tests/report.py b/tests/report.py new file mode 100644 index 0000000..6fe2612 --- /dev/null +++ b/tests/report.py @@ -0,0 +1,47 @@ + +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import plot_several_lines + +def f(X): + return 50*np.sum(X) - np.sum(np.sqrt(X) * np.sin(X)) + +dim = 25 +varbound = [[0 ,10]]*dim + +model = ga(function=f, dimension=dim, + variable_type='real', variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': 600 + } +) + +# here model exists and has checked_reports field +# now u can append any functions to report + +model.checked_reports.extend( + [ + ('report_average', np.mean), + ('report_25', lambda arr: np.quantile(arr, 0.25)), + ('report_50', np.median) + ] +) + +# run optimization process +model.run(no_plot = False) + +# now u have not only model.report but model.report_25 and so on + +#plot reports +names = [name for name, _ in model.checked_reports[::-1]] +plot_several_lines( + lines=[getattr(model, name) for name in names], + colors=('green', 'black', 'red', 'blue'), + labels=['median value', '25% quantile', 'mean of population', 'best pop score'], + linewidths=(1, 1.5, 1, 2), + title="Several custom reports with base reports", + save_as='./output/report.png' +) + + diff --git a/tests/revolution.py b/tests/revolution.py new file mode 100644 index 0000000..6f6368c --- /dev/null +++ b/tests/revolution.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Dec 18 20:05:30 2020 + +@author: qtckp +""" + + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from OppOpPopInit import OppositionOperators +from OptimizationTestFunctions import Eggholder + +from geneticalgorithm2 import geneticalgorithm2 as ga + + + +dim = 15 +np.random.seed(3) + +func = Eggholder(dim = dim) + +iterations = 1000 + +varbound = np.array([[-500,500]]*dim) + +model = ga( + function=func, dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': iterations, + 'population_size': 400, + } +) + + +start_pop = np.random.uniform(low = -500, high = 500, size = (400, dim)) + +start_gen = (start_pop, None) + +# default running + +model.run(no_plot = True, start_generation=start_gen) +plt.plot(model.report, label = 'without revolution') + +# revolutions + +model.run(no_plot = True, + start_generation=start_gen, + revolution_after_stagnation_step = 80, + revolution_part= 0.2, + revolution_oppositor = OppositionOperators.Continual.quasi(minimums = varbound[:,0], maximums = varbound[:, 1]) + ) +plt.plot(model.report, label = 'with revolution (quasi)') + + +model.run(no_plot = True, + start_generation=start_gen, + revolution_after_stagnation_step = 80, + revolution_part=0.2, + revolution_oppositor=OppositionOperators.Continual.quasi_reflect(minimums = varbound[:,0], maximums = varbound[:, 1]) + ) +plt.plot(model.report, label = 'with revolution (quasi_reflect)') + + + +plt.xlabel('Generation') +plt.ylabel('Minimized function') +plt.title('Revolution') +plt.legend() + + +plt.savefig("./output/revolution.png", dpi = 300) +plt.show() \ No newline at end of file diff --git a/tests/save and load generation.py b/tests/save and load generation.py new file mode 100644 index 0000000..d804ff9 --- /dev/null +++ b/tests/save and load generation.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jan 9 18:41:47 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + + +from OptimizationTestFunctions import Eggholder + + +dim = 2*15 + +f = Eggholder(dim) + +xmin, xmax, ymin, ymax = f.bounds + +varbound = np.array([[xmin, xmax], [ymin, ymax]]*15) + + +model = ga(function=f, + dimension = dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = { + 'max_num_iteration': 300, + 'population_size': 100 + }) + +# run and save last generation to file +filename = "./output/eggholder_lastgen.npz" +model.run(save_last_generation_as=filename) + + +# load start generation from file +model.run(start_generation=filename) + + diff --git a/tests/selections.py b/tests/selections.py new file mode 100644 index 0000000..669a5a3 --- /dev/null +++ b/tests/selections.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Created on Sun Nov 29 22:55:59 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +import matplotlib.pyplot as plt + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Selection + +def f(X): + return np.sum(X) + +dim = 50 + +varbound = [[0,10]]*dim + +selections = [ + (Selection.fully_random(),'fully_random'), + (Selection.roulette(),'roulette'), + (Selection.stochastic(),'stochastic'), + (Selection.sigma_scaling(epsilon = 0.05),'sigma_scaling; epsilon = 0.05'), + (Selection.ranking(),'ranking'), + (Selection.linear_ranking(selection_pressure = 1.5),'linear_ranking; selection_pressure = 1.5'), + (Selection.linear_ranking(selection_pressure = 1.9),'linear_ranking; selection_pressure = 1.9'), + (Selection.tournament(tau = 2),'tournament; size = 2'), + (Selection.tournament(tau = 4),'tournament; size = 4') +] + + +start_gen = np.random.uniform(0, 10, (100, dim)) + + +for sel, lab in selections: + + model = ga( + function=f, dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = { + 'max_num_iteration': 400, + 'selection_type': sel + } + ) + + model.run(no_plot = True, start_generation=(start_gen, None)) + + plt.plot(model.report, label = lab) + + +plt.xlabel('Generation') +plt.ylabel('Minimized function (sum of array)') +plt.title('Several selection types for one task') +plt.legend(fontsize=8) + + +plt.savefig("./output/selections.png", dpi = 300) +plt.show() \ No newline at end of file diff --git a/tests/set_functions.py b/tests/set_functions.py new file mode 100644 index 0000000..1eeeb2f --- /dev/null +++ b/tests/set_functions.py @@ -0,0 +1,82 @@ + +import math +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + + +def f_slow(X): + """ + slow function + """ + a = X[0] + b = X[1] + c = X[2] + s = 0 + for i in range(10000): + s += math.sin(a * i) + math.sin(b * i) + math.cos(c * i) + + return s + +rg = np.arange(10000) +def f_fast(X): + """ + fast function + """ + a, b, c = X + return (np.sin(rg*a) + np.sin(rg*b) + np.cos(rg * c)).sum() + + +algorithm_param = {'max_num_iteration': 50, + 'population_size': 100, + 'mutation_probability': 0.1, + 'elit_ratio': 0.01, + 'parents_portion': 0.3, + 'crossover_type': 'uniform', + 'mutation_type': 'uniform_by_center', + 'selection_type': 'roulette', + 'max_iteration_without_improv': None} + +varbound = [(-10, 10)] * 3 + +model = ga(function=f_slow, dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=algorithm_param) + +######## compare parallel and normal with slow function + +%time model.run() +# Wall time: 34.7s + +%time model.run(set_function=ga.set_function_multiprocess(f_slow, n_jobs=3)) +# Wall time: 23 s + + +######## compare default and vectorized on fast func and small pop + +model = ga(function=f_fast, dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=algorithm_param) + +%timeit model.run(set_function=ga.default_set_function(f_fast), no_plot=True, progress_bar_stream=None, disable_printing=True) +# 1.41 s ± 4.79 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) + +%timeit model.run(set_function=ga.vectorized_set_function(f_fast), no_plot=True, progress_bar_stream=None, disable_printing=True) +# 1.42 s ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) + +######## compare default and vectorized on fast func and big pop +algorithm_param['population_size'] = 1500 +algorithm_param['max_num_iteration'] = 15 +model = ga(function=f_fast, dimension=3, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters=algorithm_param) + +%timeit model.run(set_function=ga.default_set_function(f_fast), no_plot=True, progress_bar_stream=None, disable_printing=True) +# 6.63 s ± 229 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) + +%timeit model.run(set_function=ga.vectorized_set_function(f_fast), no_plot=True, progress_bar_stream=None, disable_printing=True) +# 6.47 s ± 87.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) + + diff --git a/tests/small_middle_callbacks.py b/tests/small_middle_callbacks.py new file mode 100644 index 0000000..3bb1198 --- /dev/null +++ b/tests/small_middle_callbacks.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jan 28 14:01:49 2021 + +@author: qtckp +""" + + +import sys +sys.path.append('..') + + +import numpy as np + +from geneticalgorithm2 import geneticalgorithm2 as ga +from geneticalgorithm2 import Actions, ActionConditions, MiddleCallbacks +from geneticalgorithm2 import Crossover, Mutations + + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*20 + +model = ga(function=f, + dimension=20, + variable_type='real', + variable_boundaries=varbound) + +model.run( + no_plot = False, + + middle_callbacks = [ + #MiddleCallbacks.UniversalCallback(Actions.Stop(), ActionConditions.EachGen(30)), + #MiddleCallbacks.UniversalCallback(Actions.ReduceMutationProb(reduce_coef = 0.98), ActionConditions.EachGen(30)), + MiddleCallbacks.UniversalCallback( + Actions.ChangeRandomCrossover([ + Crossover.shuffle(), + Crossover.two_point() + ]), + ActionConditions.EachGen(30) + ), + MiddleCallbacks.UniversalCallback( + Actions.ChangeRandomMutation([ + Mutations.uniform_by_x(), + Mutations.gauss_by_x() + ]), + ActionConditions.EachGen(50) + ) + ] +) + + diff --git a/tests/start_gen.py b/tests/start_gen.py new file mode 100644 index 0000000..5b048f3 --- /dev/null +++ b/tests/start_gen.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Nov 24 00:18:17 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + +dim = 6 + +varbound = [(0,10)]*dim + + +algorithm_param = {'max_num_iteration': 500, + 'population_size':100, + 'mutation_probability':0.1, + 'elit_ratio': 0.01, + 'crossover_probability': 0.5, + 'parents_portion': 0.3, + 'crossover_type':'uniform', + 'max_iteration_without_improv':None} + +model = ga(function=f, + dimension=dim, + variable_type='real', + variable_boundaries=varbound, + algorithm_parameters = algorithm_param) + +# start generation +# as u see u can use any values been valid for ur function +samples = np.random.uniform(0, 50, (300, dim)) # 300 is the new size of your generation + + + +model.run(no_plot = False, start_generation={'variables':samples, 'scores': None}) +# it's not necessary to evaluate scores before +# but u can do it if u have evaluated scores and don't wanna repeat calcucations + + +# from version 6.3.0 it's recommended to use this form +from geneticalgorithm2 import Generation +model.run(no_plot = False, start_generation=Generation(variables = samples, scores = None)) + + +# from version 6.4.0 u also can use these forms +model.run(no_plot = False, start_generation= samples) +model.run(no_plot = False, start_generation= (samples, None)) + + +# if u have scores array, u can put it too +scores = np.array([f(sample) for sample in samples]) +model.run(no_plot = False, start_generation= (samples, scores)) + + +# okay, let's continue optimization using saved last generation + +model.run(no_plot = False, start_generation=model.result.last_generation) diff --git a/tests/str.py b/tests/str.py new file mode 100644 index 0000000..c507e39 --- /dev/null +++ b/tests/str.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Feb 20 10:52:41 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*20 + +model = ga(function=f, dimension=20, variable_type='real', variable_boundaries=varbound) + + +print(str(model)) + + diff --git a/tests/studEA.py b/tests/studEA.py new file mode 100644 index 0000000..e4ecf65 --- /dev/null +++ b/tests/studEA.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Dec 7 02:44:17 2020 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga +import matplotlib.pyplot as plt + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*20 + +start_gen = np.random.uniform(0, 10, (100, 20)) + +model = ga(function=f, dimension=20, variable_type='real', + variable_boundaries=varbound, + algorithm_parameters={ + 'max_num_iteration': 400 + }) + + + +for stud in (False, True): + + model.run(no_plot = True, studEA= stud, start_generation=(start_gen, None), seed=1) + + plt.plot(model.report, label = f"studEA strategy = {stud}") + + + +plt.xlabel('Generation') +plt.ylabel('Minimized function') +plt.title('Using stud EA strategy') +plt.legend() + + +plt.savefig("./output/studEA.png", dpi = 300) +plt.show() diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..78a77b0 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,12 @@ + + +import os +import subprocess + + + +for file in os.listdir('./'): + if file.endswith('.py') and file != __file__: + + subprocess.call(file, shell=True) + diff --git a/tests/test_functions_code_list.py b/tests/test_functions_code_list.py new file mode 100644 index 0000000..5fe6aef --- /dev/null +++ b/tests/test_functions_code_list.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Dec 29 00:16:09 2020 + +@author: qtckp +""" + +from OptimizationTestFunctions import Sphere, Ackley, AckleyTest, Rosenbrock, Fletcher, Griewank, Penalty2, Quartic, Rastrigin, SchwefelDouble, SchwefelMax, SchwefelAbs, SchwefelSin, Stairs, Abs, Michalewicz, Scheffer, Eggholder, Weierstrass + + +lines = [] + + +dim = 2 + +functions = [ + Sphere(dim, degree = 2), + Ackley(dim), + AckleyTest(dim), + Rosenbrock(dim), + Fletcher(dim, seed = 1488), + Griewank(dim), + Penalty2(dim), + Quartic(dim), + Rastrigin(dim), + SchwefelDouble(dim), + SchwefelMax(dim), + SchwefelAbs(dim), + SchwefelSin(dim), + Stairs(dim), + Abs(dim), + Michalewicz(), + Scheffer(dim), + Eggholder(dim), + Weierstrass(dim) + ] + + + +for f in functions: + + name = type(f).__name__ + + lines.append(fr"### [{name}](https://github.com/PasaOpasen/OptimizationTestFunctions#{name.lower()})") + + lines.append(fr"![](https://github.com/PasaOpasen/OptimizationTestFunctions/blob/main/tests/heatmap%20for%20{name}.png)") + + lines.append(fr"![](tests/output/opt_test_funcs/Optimization%20process%20for%20{name}.png)") + + lines.append('') + + +with open('./output/opt_test_funcs/optimization_test_func_code_for_md.txt', 'w') as file: + file.writelines([line + '\n' for line in lines]) + + diff --git a/tests/time_limit.py b/tests/time_limit.py new file mode 100644 index 0000000..7f3d4ad --- /dev/null +++ b/tests/time_limit.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Jan 1 14:24:09 2021 + +@author: qtckp +""" + +import sys +sys.path.append('..') + + +import numpy as np +from geneticalgorithm2 import geneticalgorithm2 as ga + +def f(X): + return np.sum(X) + + +varbound = [[0,10]]*20 + +model = ga(function=f, dimension=20, variable_type='real', variable_boundaries=varbound) + +model.run(no_plot = False, + time_limit_secs = 3) + + +from truefalsepython import time_to_seconds + +model.run(no_plot = False, + time_limit_secs = time_to_seconds(minutes = 0.5, seconds = 2)) + diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..d64d7b7 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +6.8.7 \ No newline at end of file