Skip to content

Commit 1305536

Browse files
committed
Apply constraints in initial population
1 parent 01a614b commit 1305536

File tree

9 files changed

+187
-106
lines changed

9 files changed

+187
-106
lines changed

example.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def fitness_func(ga_instance, solution, solution_idx):
1717
num_genes=num_genes,
1818
mutation_num_genes=6,
1919
fitness_func=fitness_func,
20+
init_range_low=4,
21+
init_range_high=10,
2022
# suppress_warnings=True,
2123
random_mutation_min_val=4,
2224
random_mutation_max_val=10,
@@ -25,4 +27,4 @@ def fitness_func(ga_instance, solution, solution_idx):
2527
# mutation_probability=0.4,
2628
gene_constraint=[lambda x: x[0]>=8,None,None,None,None,None])
2729

28-
ga_instance.run()
30+
# ga_instance.run()
514 Bytes
Binary file not shown.

pygad/helper/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from pygad.helper import unique
2+
from pygad.helper import misc
23

3-
__version__ = "1.1.0"
4+
__version__ = "1.2.0"
27 Bytes
Binary file not shown.
4.82 KB
Binary file not shown.

pygad/helper/misc.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
The pygad.helper.helper module has some generic helper methods.
3+
"""
4+
5+
import numpy
6+
import warnings
7+
import random
8+
import pygad
9+
10+
class Helper:
11+
12+
def get_random_mutation_range(self, gene_index):
13+
14+
"""
15+
Returns the minimum and maximum values of the mutation range.
16+
It accepts a single parameter:
17+
-gene_index: The index of the gene to get its range. Only used if the gene has a specific mutation range
18+
It returns the minimum and maximum values of the gene mutation range.
19+
"""
20+
21+
# We can use either random_mutation_min_val or random_mutation_max_val.
22+
if type(self.random_mutation_min_val) in self.supported_int_float_types:
23+
range_min = self.random_mutation_min_val
24+
range_max = self.random_mutation_max_val
25+
else:
26+
range_min = self.random_mutation_min_val[gene_index]
27+
range_max = self.random_mutation_max_val[gene_index]
28+
return range_min, range_max
29+
30+
def get_initial_population_range(self, gene_index):
31+
32+
"""
33+
Returns the minimum and maximum values of the initial population range.
34+
It accepts a single parameter:
35+
-gene_index: The index of the gene to get its range. Only used if the gene has a specific range
36+
It returns the minimum and maximum values of the gene initial population range.
37+
"""
38+
39+
# We can use either init_range_low or init_range_high.
40+
if type(self.init_range_low) in self.supported_int_float_types:
41+
range_min = self.init_range_low
42+
range_max = self.init_range_high
43+
else:
44+
range_min = self.init_range_low[gene_index]
45+
range_max = self.init_range_high[gene_index]
46+
return range_min, range_max
47+
48+
def generate_gene_random_value(self,
49+
range_min,
50+
range_max,
51+
gene_value,
52+
gene_idx,
53+
mutation_by_replacement,
54+
num_values=1):
55+
"""
56+
Randomly generate one or more values for the gene.
57+
It accepts:
58+
-range_min: The minimum value in the range from which a value is selected.
59+
-range_max: The maximum value in the range from which a value is selected.
60+
-gene_value: The original gene value before applying mutation.
61+
-gene_idx: The index of the gene in the solution.
62+
-mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False.
63+
-num_values: The number of random valus to generate. It tries to generate a number of values up to a maximum of num_values. But it is not always guranteed because the total number of values might not be enough or the random generator creates duplicate random values.
64+
If num_values=1, it returns a single numeric value. If num_values>1, it returns an array with number of values equal to num_values.
65+
"""
66+
67+
# Generating a random value.
68+
random_value = numpy.asarray(numpy.random.uniform(low=range_min,
69+
high=range_max,
70+
size=num_values),
71+
dtype=object)
72+
73+
# Change the random mutation value data type.
74+
for idx, val in enumerate(random_value):
75+
random_value[idx] = self.change_random_mutation_value_dtype(random_value[idx],
76+
gene_idx,
77+
gene_value,
78+
mutation_by_replacement=mutation_by_replacement)
79+
80+
# Round the gene.
81+
random_value[idx] = self.round_random_mutation_value(random_value[idx], gene_idx)
82+
83+
# Rounding different values could return the same value multiple times.
84+
# For example, 2.8 and 2.7 will be 3.0.
85+
# Use the unique() function to avoid any duplicates.
86+
random_value = numpy.unique(random_value)
87+
88+
if num_values == 1:
89+
random_value = random_value[0]
90+
91+
return random_value
92+
93+
def get_valid_gene_constraint_values(self,
94+
range_min,
95+
range_max,
96+
gene_value,
97+
gene_idx,
98+
mutation_by_replacement,
99+
solution,
100+
num_values=100):
101+
"""
102+
Randomly generate values for the gene that satisfy the constraint.
103+
It accepts:
104+
-range_min: The minimum value in the range from which a value is selected.
105+
-range_max: The maximum value in the range from which a value is selected.
106+
-gene_value: The original gene value before applying mutation.
107+
-gene_idx: The index of the gene in the solution.
108+
-mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False.
109+
-solution: The solution in which the gene exists.
110+
-num_values: The number of random valus to generate. It tries to generate a number of values up to a maximum of num_values. But it is not always guranteed because the total number of values might not be enough or the random generator creates duplicate random values.
111+
If num_values=1, it returns a single numeric value. If num_values>1, it returns an array with number of values equal to num_values.
112+
"""
113+
random_values = self.generate_gene_random_value(range_min=range_min,
114+
range_max=range_max,
115+
gene_value=gene_value,
116+
gene_idx=gene_idx,
117+
mutation_by_replacement=mutation_by_replacement,
118+
num_values=num_values)
119+
random_values_filtered = self.mutation_filter_values_by_constraint(random_values=random_values,
120+
solution=solution,
121+
gene_idx=gene_idx)
122+
return random_values_filtered

pygad/pygad.py

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class GA(utils.parent_selection.ParentSelection,
1515
utils.mutation.Mutation,
1616
utils.nsga2.NSGA2,
1717
helper.unique.Unique,
18+
helper.misc.Helper,
1819
visualize.plot.Plot):
1920

2021
supported_int_types = [int, numpy.int8, numpy.int16, numpy.int32, numpy.int64,
@@ -435,7 +436,8 @@ def __init__(self,
435436
high=self.init_range_high,
436437
allow_duplicate_genes=allow_duplicate_genes,
437438
mutation_by_replacement=True,
438-
gene_type=self.gene_type)
439+
gene_type=self.gene_type,
440+
gene_constraint=gene_constraint)
439441
else:
440442
self.valid_parameters = False
441443
raise TypeError(f"The expected type of both the sol_per_pop and num_genes parameters is int but {type(sol_per_pop)} and {type(num_genes)} found.")
@@ -1377,12 +1379,18 @@ def initialize_population(self,
13771379
high,
13781380
allow_duplicate_genes,
13791381
mutation_by_replacement,
1380-
gene_type):
1382+
gene_type,
1383+
gene_constraint):
13811384
"""
13821385
Creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named 'population'.
13831386
1384-
low: The lower value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20 and higher.
1385-
high: The upper value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20.
1387+
It accepts:
1388+
-low: The lower value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20 and higher.
1389+
-high: The upper value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20.
1390+
-allow_duplicate_genes: Whether duplicate genes are allowed or not.
1391+
-mutation_by_replacement: Whether mutation by replacement is enabled or not.
1392+
-gene_type: The data type of the genes.
1393+
-gene_constraint: The constraints of the genes.
13861394
13871395
This method assigns the values of the following 3 instance attributes:
13881396
1. pop_size: Size of the population.
@@ -1397,23 +1405,19 @@ def initialize_population(self,
13971405
if self.gene_space is None:
13981406
# Creating the initial population randomly.
13991407
if self.gene_type_single == True:
1408+
# A NumPy array holding the initial population.
14001409
self.population = numpy.asarray(numpy.random.uniform(low=low,
14011410
high=high,
14021411
size=self.pop_size),
1403-
dtype=self.gene_type[0]) # A NumPy array holding the initial population.
1412+
dtype=self.gene_type[0])
14041413
else:
14051414
# Create an empty population of dtype=object to support storing mixed data types within the same array.
14061415
self.population = numpy.zeros(
14071416
shape=self.pop_size, dtype=object)
14081417
# Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population.
14091418
for gene_idx in range(self.num_genes):
14101419

1411-
if type(self.init_range_low) in self.supported_int_float_types:
1412-
range_min = self.init_range_low
1413-
range_max = self.init_range_high
1414-
else:
1415-
range_min = self.init_range_low[gene_idx]
1416-
range_max = self.init_range_high[gene_idx]
1420+
range_min, range_max = self.get_initial_population_range(gene_index=gene_idx)
14171421

14181422
# A vector of all values of this single gene across all solutions in the population.
14191423
gene_values = numpy.asarray(numpy.random.uniform(low=range_min,
@@ -1423,6 +1427,30 @@ def initialize_population(self,
14231427
# Adding the current gene values to the population.
14241428
self.population[:, gene_idx] = gene_values
14251429

1430+
# Enforce the gene constraints as much as possible.
1431+
if gene_constraint is None:
1432+
pass
1433+
else:
1434+
# Note that gene_constraint is not validated yet.
1435+
# We have to set it as a propery of the pygad.GA instance to retrieve without passing it as an additional parameter.
1436+
self.gene_constraint = gene_constraint
1437+
for solution in self.population:
1438+
for gene_idx in range(self.num_genes):
1439+
# Check that a constraint is available for the gene and that the current value does not satisfy that constraint
1440+
if self.gene_constraint[gene_idx]:
1441+
print(gene_idx, solution[gene_idx])
1442+
if not self.gene_constraint[gene_idx](solution):
1443+
range_min, range_max = self.get_initial_population_range(gene_index=gene_idx)
1444+
# While initializing the population, we follow a mutation by replacement approach. So, the gene value is not needed.
1445+
random_values_filtered = self.get_valid_gene_constraint_values(range_min=range_min,
1446+
range_max=range_max,
1447+
gene_value=None,
1448+
gene_idx=gene_idx,
1449+
mutation_by_replacement=True,
1450+
solution=solution,
1451+
num_values=100)
1452+
print(gene_idx, random_values_filtered)
1453+
14261454
if allow_duplicate_genes == False:
14271455
for solution_idx in range(self.population.shape[0]):
14281456
# self.logger.info("Before", self.population[solution_idx])
@@ -1444,12 +1472,7 @@ def initialize_population(self,
14441472
for sol_idx in range(self.sol_per_pop):
14451473
for gene_idx in range(self.num_genes):
14461474

1447-
if type(self.init_range_low) in self.supported_int_float_types:
1448-
range_min = self.init_range_low
1449-
range_max = self.init_range_high
1450-
else:
1451-
range_min = self.init_range_low[gene_idx]
1452-
range_max = self.init_range_high[gene_idx]
1475+
range_min, range_max = self.get_initial_population_range(gene_index=gene_idx)
14531476

14541477
if self.gene_space[gene_idx] is None:
14551478

@@ -1524,12 +1547,7 @@ def initialize_population(self,
15241547
for sol_idx in range(self.sol_per_pop):
15251548
for gene_idx in range(self.num_genes):
15261549

1527-
if type(self.init_range_low) in self.supported_int_float_types:
1528-
range_min = self.init_range_low
1529-
range_max = self.init_range_high
1530-
else:
1531-
range_min = self.init_range_low[gene_idx]
1532-
range_max = self.init_range_high[gene_idx]
1550+
range_min, range_max = self.get_initial_population_range(gene_index=gene_idx)
15331551

15341552
if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple, range]:
15351553
# Convert to list because tuple and range do not have copy().
@@ -1584,12 +1602,7 @@ def initialize_population(self,
15841602
# Replace all the None values with random values using the init_range_low, init_range_high, and gene_type attributes.
15851603
for gene_idx, curr_gene_space in enumerate(self.gene_space):
15861604

1587-
if type(self.init_range_low) in self.supported_int_float_types:
1588-
range_min = self.init_range_low
1589-
range_max = self.init_range_high
1590-
else:
1591-
range_min = self.init_range_low[gene_idx]
1592-
range_max = self.init_range_high[gene_idx]
1605+
range_min, range_max = self.get_initial_population_range(gene_index=gene_idx)
15931606

15941607
if curr_gene_space is None:
15951608
self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=range_min,
-1.39 KB
Binary file not shown.

0 commit comments

Comments
 (0)