Skip to content

Commit fe5b002

Browse files
authored
Merge pull request #265 from ahmedfgad/github-actions
GitHub actions
2 parents e946f04 + b3183c2 commit fe5b002

12 files changed

+329
-56
lines changed

‎docs/source/conf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
# -- Project information -----------------------------------------------------
1919

2020
project = 'PyGAD'
21-
copyright = '2023, Ahmed Fawzy Gad'
21+
copyright = '2024, Ahmed Fawzy Gad'
2222
author = 'Ahmed Fawzy Gad'
2323

2424
# The full version, including alpha/beta/rc tags
25-
release = '3.2.0'
25+
release = '3.3.0'
2626

2727
master_doc = 'index'
2828

‎docs/source/pygad.rst

+8-10
Original file line numberDiff line numberDiff line change
@@ -920,9 +920,7 @@ It accepts the following parameters:
920920
- ``pop_fitness=None``: An optional parameter that accepts a list of
921921
the fitness values of the solutions in the population. If ``None``,
922922
then the ``cal_pop_fitness()`` method is called to calculate the
923-
fitness values of the ``self.population``. Use
924-
``ga_instance.last_generation_fitness`` to use latest fitness value
925-
and skip recalculation of the population fitness.
923+
fitness values of the population.
926924

927925
It returns the following:
928926

@@ -1060,15 +1058,15 @@ optimization problem is single-objective or multi-objective.
10601058
``pygad.GA`` class.
10611059

10621060
- If the fitness function returns a ``list``, ``tuple``, or
1063-
``numpy.ndarray``, then the problem is multi-objective. Even if
1064-
there is only one element, the problem is still considered
1065-
multi-objective. Each element represents the fitness value of its
1066-
corresponding objective.
1061+
``numpy.ndarray``, then the problem is multi-objective. Even if there
1062+
is only one element, the problem is still considered multi-objective.
1063+
Each element represents the fitness value of its corresponding
1064+
objective.
10671065

10681066
Using a user-defined fitness function allows the user to freely use
1069-
PyGAD solves any problem by passing the appropriate fitness
1070-
function/method. It is very important to understand the problem well before
1071-
creating it.
1067+
PyGAD to solve any problem by passing the appropriate fitness
1068+
function/method. It is very important to understand the problem well
1069+
before creating it.
10721070

10731071
Let's discuss an example:
10741072

‎docs/source/pygad_more.rst

+104-5
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,13 @@ is applied based on this parameter.
344344
How Mutation Works with the ``gene_space`` Parameter?
345345
-----------------------------------------------------
346346

347-
If a gene has its static space defined in the ``gene_space`` parameter,
348-
then mutation works by replacing the gene value by a value randomly
349-
selected from the gene space. This happens for both ``int`` and
350-
``float`` data types.
347+
Mutation changes based on whether the ``gene_space`` has a continuous
348+
range or discrete set of values.
349+
350+
If a gene has its **static/discrete space** defined in the
351+
``gene_space`` parameter, then mutation works by replacing the gene
352+
value by a value randomly selected from the gene space. This happens for
353+
both ``int`` and ``float`` data types.
351354

352355
For example, the following ``gene_space`` has the static space
353356
``[1, 2, 3]`` defined for the first gene. So, this gene can only have a
@@ -377,6 +380,39 @@ If its current value is 5 and the random value is ``-0.5``, then the new
377380
value is 4.5. If the gene type is integer, then the value will be
378381
rounded.
379382

383+
On the other hand, if a gene has a **continuous space** defined in the
384+
``gene_space`` parameter, then mutation occurs by adding a random value
385+
to the current gene value.
386+
387+
For example, the following ``gene_space`` has the continuous space
388+
defined by the dictionary ``{'low': 1, 'high': 5}``. This applies to all
389+
genes. So, mutation is applied to one or more selected genes by adding a
390+
random value to the current gene value.
391+
392+
.. code:: python
393+
394+
Gene space: {'low': 1, 'high': 5}
395+
Solution: [1.5, 3.4]
396+
397+
Assuming ``random_mutation_min_val=-1`` and
398+
``random_mutation_max_val=1``, then a random value such as ``0.3`` can
399+
be added to the gene(s) participating in mutation. If only the first
400+
gene is mutated, then its new value changes from ``1.5`` to
401+
``1.5+0.3=1.8``. Note that PyGAD verifies that the new value is within
402+
the range. In the worst scenarios, the value will be set to either
403+
boundary of the continuous range. For example, if the gene value is 1.5
404+
and the random value is -0.55, then the new value is 0.95 which smaller
405+
than the lower boundary 1. Thus, the gene value will be rounded to 1.
406+
407+
If the dictionary has a step like the example below, then it is
408+
considered a discrete range and mutation occurs by randomly selecting a
409+
value from the set of values. In other words, no random value is added
410+
to the gene value.
411+
412+
.. code:: python
413+
414+
Gene space: {'low': 1, 'high': 5, 'step': 0.5}
415+
380416
Stop at Any Generation
381417
======================
382418

@@ -596,7 +632,6 @@ After running the code again, it will find the same result.
596632
0.04872203136549972
597633
598634
Continue without Losing Progress
599-
=================================
600635

601636
In `PyGAD
602637
2.18.0 <https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0>`__,
@@ -663,6 +698,70 @@ Note that the 2 attributes (``self.best_solutions`` and
663698
attributes (``self.solutions`` and ``self.solutions_fitness``) only work
664699
if the ``save_solutions`` parameter is ``True``.
665700

701+
Change Population Size during Runtime
702+
=====================================
703+
704+
Starting from `PyGAD
705+
3.3.0 <https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0>`__,
706+
the population size can changed during runtime. In other words, the
707+
number of solutions/chromosomes and number of genes can be changed.
708+
709+
The user has to carefully arrange the list of *parameters* and *instance
710+
attributes* that have to be changed to keep the GA consistent before and
711+
after changing the population size. Generally, change everything that
712+
would be used during the GA evolution.
713+
714+
CAUTION: If the user failed to change a parameter or an instance
715+
attributes necessary to keep the GA running after the population size
716+
changed, errors will arise.
717+
718+
These are examples of the parameters that the user should decide whether
719+
to change. The user should check the `list of
720+
parameters <https://pygad.readthedocs.io/en/latest/pygad.html#init>`__
721+
and decide what to change.
722+
723+
1. ``population``: The population. It *must* be changed.
724+
725+
2. ``num_offspring``: The number of offspring to produce out of the
726+
crossover and mutation operations. Change this parameter if the
727+
number of offspring have to be changed to be consistent with the new
728+
population size.
729+
730+
3. ``num_parents_mating``: The number of solutions to select as parents.
731+
Change this parameter if the number of parents have to be changed to
732+
be consistent with the new population size.
733+
734+
4. ``fitness_func``: If the way of calculating the fitness changes after
735+
the new population size, then the fitness function have to be
736+
changed.
737+
738+
5. ``sol_per_pop``: The number of solutions per population. It is not
739+
critical to change it but it is recommended to keep this number
740+
consistent with the number of solutions in the ``population``
741+
parameter.
742+
743+
These are examples of the instance attributes that might be changed. The
744+
user should check the `list of instance
745+
attributes <https://pygad.readthedocs.io/en/latest/pygad.html#other-instance-attributes-methods>`__
746+
and decide what to change.
747+
748+
1. All the ``last_generation_*`` parameters
749+
750+
1. ``last_generation_fitness``: A 1D NumPy array of fitness values of
751+
the population.
752+
753+
2. ``last_generation_parents`` and
754+
``last_generation_parents_indices``: Two NumPy arrays: 2D array
755+
representing the parents and 1D array of the parents indices.
756+
757+
3. ``last_generation_elitism`` and
758+
``last_generation_elitism_indices``: Must be changed if
759+
``keep_elitism != 0``. The default value of ``keep_elitism`` is 1.
760+
Two NumPy arrays: 2D array representing the elitism and 1D array
761+
of the elitism indices.
762+
763+
2. ``pop_size``: The population size.
764+
666765
Prevent Duplicates in Gene Values
667766
=================================
668767

‎docs/source/releases.rst

+68
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,74 @@ Release Date 7 September 2023
14641464
class is removed. Instead, please use the ``plot_fitness()`` if you
14651465
did not upgrade yet.
14661466

1467+
.. _pygad-330:
1468+
1469+
PyGAD 3.3.0
1470+
-----------
1471+
1472+
Release Date 29 January 2024
1473+
1474+
1. Solve bugs when multi-objective optimization is used.
1475+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238
1476+
1477+
2. When the ``stop_ciiteria`` parameter is used with the ``reach``
1478+
keyword, then multiple numeric values can be passed when solving a
1479+
multi-objective problem. For example, if a problem has 3 objective
1480+
functions, then ``stop_criteria="reach_10_20_30"`` means the GA
1481+
stops if the fitness of the 3 objectives are at least 10, 20, and
1482+
30, respectively. The number values must match the number of
1483+
objective functions. If a single value found (e.g.
1484+
``stop_criteria=reach_5``) when solving a multi-objective problem,
1485+
then it is used across all the objectives.
1486+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238
1487+
1488+
3. The ``delay_after_gen`` parameter is now deprecated and will be
1489+
removed in a future release. If it is necessary to have a time delay
1490+
after each generation, then assign a callback function/method to the
1491+
``on_generation`` parameter to pause the evolution.
1492+
1493+
4. Parallel processing now supports calculating the fitness during
1494+
adaptive mutation.
1495+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201
1496+
1497+
5. The population size can be changed during runtime by changing all
1498+
the parameters that would affect the size of any thing used by the
1499+
GA. For more information, check the `Change Population Size during
1500+
Runtime <https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime>`__
1501+
section.
1502+
https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234
1503+
1504+
6. When a dictionary exists in the ``gene_space`` parameter without a
1505+
step, then mutation occurs by adding a random value to the gene
1506+
value. The random vaue is generated based on the 2 parameters
1507+
``random_mutation_min_val`` and ``random_mutation_max_val``. For
1508+
more information, check the `How Mutation Works with the gene_space
1509+
Parameter? <https://pygad.readthedocs.io/en/latest/pygad_more.html#how-mutation-works-with-the-gene-space-parameter>`__
1510+
section.
1511+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229
1512+
1513+
7. Add ``object`` as a supported data type for int
1514+
(GA.supported_int_types) and float (GA.supported_float_types).
1515+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/174
1516+
1517+
8. Use the ``raise`` clause instead of the ``sys.exit(-1)`` to
1518+
terminate the execution.
1519+
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/213
1520+
1521+
9. Fix a bug when multi-objective optimization is used with batch
1522+
fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero
1523+
number).
1524+
1525+
10. Fix a bug in the ``pygad.py`` script when finding the index of the
1526+
best solution. It does not work properly with multi-objective
1527+
optimization where ``self.best_solutions_fitness`` have multiple
1528+
columns.
1529+
1530+
.. code:: python
1531+
1532+
self.best_solution_generation = numpy.where(numpy.array(
1533+
self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0]
1534+
14671535
PyGAD Projects at GitHub
14681536
========================
14691537

‎examples/example_dynamic_population_size.py

+63-24
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,83 @@
33

44
"""
55
This is an example to dynamically change the population size (i.e. number of solutions/chromosomes per population) during runtime.
6-
The following 2 instance attributes must be changed to meet the new desired population size:
7-
1) population: This is a NumPy array holding the population.
8-
2) num_offspring: This represents the number of offspring to produce during crossover.
9-
For example, if the population initially has 20 solutions and 6 genes. To change it to have 30 solutions, then:
10-
1)population: Create a new NumPy array with the desired size (30, 6) and assign it to the population instance attribute.
11-
2)num_offspring: Set the num_offspring attribute accordingly (e.g. 29 assuming that keep_elitism has the default value of 1).
6+
7+
The user has to carefully inspect the parameters and instance attributes to select those that must be changed to be consistent with the new population size.
8+
Check this link for more information: https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime
129
"""
1310

11+
def update_GA(ga_i,
12+
pop_size):
13+
"""
14+
Update the parameters and instance attributes to match the new population size.
15+
16+
Parameters
17+
----------
18+
ga_i : TYPE
19+
The pygad.GA instance.
20+
pop_size : TYPE
21+
The new population size.
22+
23+
Returns
24+
-------
25+
None.
26+
"""
27+
28+
ga_i.pop_size = pop_size
29+
ga_i.sol_per_pop = ga_i.pop_size[0]
30+
ga_i.num_parents_mating = int(ga_i.pop_size[0]/2)
31+
32+
# Calculate the new value for the num_offspring parameter.
33+
if ga_i.keep_elitism != 0:
34+
ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_elitism
35+
elif ga_i.keep_parents != 0:
36+
if ga_i.keep_parents == -1:
37+
ga_i.num_offspring = ga_i.sol_per_pop - ga_i.num_parents_mating
38+
else:
39+
ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_parents
40+
41+
ga_i.num_genes = ga_i.pop_size[1]
42+
ga_i.population = numpy.random.uniform(low=ga_i.init_range_low,
43+
high=ga_i.init_range_low,
44+
size=ga_i.pop_size)
45+
fitness = []
46+
for solution, solution_idx in enumerate(ga_i.population):
47+
fitness.append(fitness_func(ga_i, solution, solution_idx))
48+
ga_i.last_generation_fitness = numpy.array(fitness)
49+
parents, parents_fitness = ga_i.steady_state_selection(ga_i.last_generation_fitness,
50+
ga_i.num_parents_mating)
51+
ga_i.last_generation_elitism = parents[:ga_i.keep_elitism]
52+
ga_i.last_generation_elitism_indices = parents_fitness[:ga_i.keep_elitism]
53+
54+
ga_i.last_generation_parents = parents
55+
ga_i.last_generation_parents_indices = parents_fitness
56+
1457
def fitness_func(ga_instance, solution, solution_idx):
15-
return [numpy.random.rand(), numpy.random.rand()]
58+
return numpy.sum(solution)
1659

1760
def on_generation(ga_i):
1861
# The population starts with 20 solutions.
19-
print(ga_i.generations_completed, ga_i.num_offspring, ga_i.population.shape)
20-
# At generation 15, increase the population size to 40 solutions.
62+
print(ga_i.generations_completed, ga_i.population.shape)
63+
# At generation 15, set the population size to 30 solutions and 10 genes.
2164
if ga_i.generations_completed >= 15:
22-
ga_i.num_offspring = 49
23-
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
24-
new_population[:ga_i.population.shape[0], :] = ga_i.population
25-
ga_i.population = new_population
65+
ga_i.pop_size = (30, 10)
66+
update_GA(ga_i=ga_i,
67+
pop_size=(30, 10))
68+
# At generation 10, set the population size to 15 solutions and 8 genes.
2669
elif ga_i.generations_completed >= 10:
27-
ga_i.num_offspring = 39
28-
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
29-
new_population[:ga_i.population.shape[0], :] = ga_i.population
30-
ga_i.population = new_population
31-
# At generation 10, increase the population size to 30 solutions.
70+
update_GA(ga_i=ga_i,
71+
pop_size=(15, 8))
72+
# At generation 5, set the population size to 10 solutions and 3 genes.
3273
elif ga_i.generations_completed >= 5:
33-
ga_i.num_offspring = 29
34-
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
35-
new_population[:ga_i.population.shape[0], :] = ga_i.population
36-
ga_i.population = new_population
74+
update_GA(ga_i=ga_i,
75+
pop_size=(10, 3))
3776

3877
ga_instance = pygad.GA(num_generations=20,
3978
sol_per_pop=20,
4079
num_genes=6,
4180
num_parents_mating=10,
4281
fitness_func=fitness_func,
43-
on_generation=on_generation,
44-
parent_selection_type='nsga2')
82+
on_generation=on_generation)
4583

4684
ga_instance.run()
85+

0 commit comments

Comments
 (0)