Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion pygad/pygad.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import concurrent.futures
import inspect
import logging

import numpy as np

from pygad import utils
from pygad import helper
from pygad import visualize
Expand All @@ -25,6 +28,8 @@ class GA(utils.parent_selection.ParentSelection,
object]
supported_int_float_types = supported_int_types + supported_float_types

boundaries = None

def __init__(self,
num_generations,
num_parents_mating,
Expand All @@ -36,6 +41,7 @@ def __init__(self,
init_range_low=-4,
init_range_high=4,
gene_type=float,
gene_structure=None,
parent_selection_type="sss",
keep_parents=-1,
keep_elitism=1,
Expand Down Expand Up @@ -85,6 +91,10 @@ def __init__(self,
# It is OK to set the value of the 2 parameters ('init_range_low' and 'init_range_high') to be equal, higher or lower than the other parameter (i.e. init_range_low is not needed to be lower than init_range_high).

gene_type: The type of the gene. It is assigned to any of these types (int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, float, numpy.float16, numpy.float32, numpy.float64) and forces all the genes to be of that type.
gene_structure: A List of containing positive integers, that indicate the number of int/floats inside a single gene_type/index of the gene
Example: num_genes=10; gene_structure=[1,2,5,1,1] -> 5 genes in total, first gene has 1 int/float, second gene has 2 int/floats, third gene has 5 int/floats, fourth gene has 1 int/float, and fifth gene has 1 int/float.
Note: The sum of the values in the 'gene_structure' list must be equal to the value assigned to the 'num_genes' parameter.


parent_selection_type: Type of parent selection.
keep_parents: If 0, this means no parent in the current population will be used in the next population. If -1, this means all parents in the current population will be used in the next population. If set to a value > 0, then the specified value refers to the number of parents in the current population to be used in the next population. Some parent selection operators such as rank selection, favor population diversity and therefore keeping the parents in the next generation can be beneficial. However, some other parent selection operators, such as roulette wheel selection (RWS), have higher selection pressure and keeping more than one parent in the next generation can seriously harm population diversity. This parameter have an effect only when the keep_elitism parameter is 0. Thanks to Prof. Fernando Jiménez (http://webs.um.es/fernan) for editing this sentence.
Expand Down Expand Up @@ -335,6 +345,14 @@ def __init__(self,
self.init_range_low = init_range_low
self.init_range_high = init_range_high

# Transform gene_structure to np.array
if (gene_structure is not None):
self.gene_structure = np.array(gene_structure)
# Example: [1, 1, 3] -> [0, 1, 2, 5]
self.boundaries = numpy.insert(numpy.cumsum(self.gene_structure), 0, 0)
else:
self.gene_structure = None

# Validate gene_type
if gene_type in GA.supported_int_float_types:
self.gene_type = [gene_type, None]
Expand Down Expand Up @@ -1351,10 +1369,15 @@ def round_genes(self, solutions):
self.gene_type[gene_idx][1])
return solutions

def check_gene_structure(self,gene_structure,num_gene):
assert (np.sum(gene_structure)==num_gene, "the sum of all integers inside the gene_structure parameter must equal the num_gene parameter")
# it is also possible to write this function in such way that we return the num_gene and therefore ensure it this way, this likely would produce more user errors

def initialize_population(self,
allow_duplicate_genes,
gene_type,
gene_constraint):
gene_constraint,
gene_structure=None):
"""
Creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named 'population'.

Expand All @@ -1363,6 +1386,9 @@ def initialize_population(self,
-gene_type: The data type of the genes.
-gene_constraint: The constraints of the genes.

-gene_structure: A List of containing positive integers, that indicate the number of int/floats inside a single gene_type/index of the gene
Example: num_genes=10; gene_structure=[1,2,5,1,1]

This method assigns the values of the following 3 instance attributes:
1. pop_size: Size of the population.
2. population: Initially, holds the initial population and later updated after each generation.
Expand Down
110 changes: 93 additions & 17 deletions pygad/utils/crossover.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,20 @@ def single_point_crossover(self, parents, offspring_size):

# Randomly generate all the K points at which crossover takes place between each two parents. The point does not have to be always at the center of the solutions.
# This saves time by calling the numpy.random.randint() function only once.
crossover_points = numpy.random.randint(low=0,
high=parents.shape[1],
size=offspring_size[0])
if self.gene_structure is None:
crossover_points = numpy.random.randint(low=0,
high=parents.shape[1],
size=offspring_size[0])
else:
# Select random boundary index excluding the 0th index and last index to ensure split?
# boundaries: [0, 2, 5, 10] -> valid cuts are 2, 5. So boundaries[1:-1]
# If structure has only 1 block, crossover point is meaningless? Standard GA allows index (0..N).
# Standard: low=0, high=N. Point K means split at index K.
# 0 -> Empty first part, Full second part.
# N -> Full first part, Empty second.
# So boundaries are valid crossover points immediately.
valid_cuts = self.boundaries
crossover_points = numpy.random.choice(valid_cuts, size=offspring_size[0])

for k in range(offspring_size[0]):
# Check if the crossover_probability parameter is used.
Expand Down Expand Up @@ -97,15 +108,54 @@ def two_points_crossover(self, parents, offspring_size):

# Randomly generate all the first K points at which crossover takes place between each two parents.
# This saves time by calling the numpy.random.randint() function only once.
if (parents.shape[1] == 1): # If the chromosome has only a single gene. In this case, this gene is copied from the second parent.
crossover_points_1 = numpy.zeros(offspring_size[0])
if self.gene_structure is None:
if (parents.shape[1] == 1):
crossover_points_1 = numpy.zeros(offspring_size[0])
else:
crossover_points_1 = numpy.random.randint(low=0,
high=numpy.ceil(parents.shape[1]/2 + 1),
size=offspring_size[0])
# The second point must always be greater than the first point.
crossover_points_2 = crossover_points_1 + int(parents.shape[1]/2)
else:
crossover_points_1 = numpy.random.randint(low=0,
high=numpy.ceil(parents.shape[1]/2 + 1),
size=offspring_size[0])

# The second point must always be greater than the first point.
crossover_points_2 = crossover_points_1 + int(parents.shape[1]/2)
num_logical = len(self.gene_structure)
if num_logical < 2:
# Can't do meaningful 2-point on < 2 blocks? Standard allows splitting anywhere.
# If only 1 block [0, 10], Boundaries are 0, 10.
# Cuts: 0, 10.
crossover_points_1 = numpy.zeros(offspring_size[0], dtype=int)
crossover_points_2 = numpy.full(offspring_size[0], parents.shape[1], dtype=int)
else:
# Select 2 distinct boundary indices.
# To ensure p1 != p2 efficiently for N offspring:
# 1. Pick p1_idx from [0, num_boundaries).
# 2. Pick offset from [1, num_boundaries).
# 3. p2_idx = (p1_idx + offset) % num_boundaries.
# This guarantees p1_idx != p2_idx.

num_boundaries = len(self.boundaries)
# We need indices [0, num_boundaries-1]

# p1 indices:
p1_idx = numpy.random.randint(low=0, high=num_boundaries, size=offspring_size[0])

# Offsets: at least 1, at most num_boundaries-1
# If num_boundaries <= 1 (impossible if num_logical >= 1, boundaries has at least 0, N),
# but if num_logical=1, boundaries=[0, N]. len=2. offset must be 1.
# randint(1, 2) -> returns [1]. Correct.

offsets = numpy.random.randint(low=1, high=num_boundaries, size=offspring_size[0])

p2_idx = (p1_idx + offsets) % num_boundaries

# Gather points
# Stack to sort
points_idx = numpy.column_stack((p1_idx, p2_idx))
points_idx.sort(axis=1)

# Map indices to actual boundary values
crossover_points_1 = self.boundaries[points_idx[:, 0]]
crossover_points_2 = self.boundaries[points_idx[:, 1]]

for k in range(offspring_size[0]):

Expand Down Expand Up @@ -172,9 +222,22 @@ def uniform_crossover(self, parents, offspring_size):
# This saves time by calling the numpy.random.randint() function only once.
# There is a list of 0 and 1 for each offspring.
# [0, 1, 0, 0, 1, 1]: If the value is 0, then take the gene from the first parent. If 1, take it from the second parent.
genes_sources = numpy.random.randint(low=0,
high=2,
size=offspring_size)
if self.gene_structure is None:
genes_sources = numpy.random.randint(low=0,
high=2,
size=offspring_size)
else:
# Generate sources for LOGICAL blocks
num_logical = len(self.gene_structure)
logical_sources = numpy.random.randint(low=0, high=2, size=(offspring_size[0], num_logical))

# Map logical sources to full gene mask
genes_sources = numpy.empty(offspring_size, dtype=int)
for k in range(offspring_size[0]):
for b_idx in range(num_logical):
start = self.boundaries[b_idx]
end = self.boundaries[b_idx+1]
genes_sources[k, start:end] = logical_sources[k, b_idx]

for k in range(offspring_size[0]):
if not (self.crossover_probability is None):
Expand Down Expand Up @@ -242,9 +305,22 @@ def scattered_crossover(self, parents, offspring_size):
# This saves time by calling the numpy.random.randint() function only once.
# There is a list of 0 and 1 for each offspring.
# [0, 1, 0, 0, 1, 1]: If the value is 0, then take the gene from the first parent. If 1, take it from the second parent.
genes_sources = numpy.random.randint(low=0,
high=2,
size=offspring_size)
if self.gene_structure is None:
genes_sources = numpy.random.randint(low=0,
high=2,
size=offspring_size)
else:
# Generate sources for LOGICAL blocks
num_logical = len(self.gene_structure)
logical_sources = numpy.random.randint(low=0, high=2, size=(offspring_size[0], num_logical))

# Map logical sources to full gene mask
genes_sources = numpy.empty(offspring_size, dtype=int)
for k in range(offspring_size[0]):
for b_idx in range(num_logical):
start = self.boundaries[b_idx]
end = self.boundaries[b_idx+1]
genes_sources[k, start:end] = logical_sources[k, b_idx]

for k in range(offspring_size[0]):
if not (self.crossover_probability is None):
Expand Down
Loading