Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize variables type usage #22

Merged
merged 1 commit into from
Feb 22, 2024
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ solver = ContinuousGenAlgSolver(
mutation_rate=0.1,
selection_rate=0.6,
selection_strategy="roulette_wheel",
problem_type=float, # Defines the possible values as float numbers
variables_type=float, # Defines the possible values as float numbers
variables_limits=(-10, 10) # Defines the limits of all variables between -10 and 10.
# Alternatively one can pass an array of tuples defining the limits
# for each variable: [(-10, 10), (0, 5), (0, 5), (-20, 20)]
Expand Down
33 changes: 24 additions & 9 deletions geneal/genetic_algorithms/_continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import numpy as np

from geneal.genetic_algorithms.genetic_algorithm_base import GenAlgSolver
from geneal.utils.exceptions import InvalidInput
from geneal.utils.helpers import get_input_dimensions


Expand All @@ -21,15 +22,15 @@
plot_results: bool = True,
excluded_genes: Sequence = None,
variables_limits=(-10, 10),
problem_type=float,
variables_type=float,
n_crossover_points: int = 1,
fitness_tolerance=None,
random_state: int = None,
):
"""
:param fitness_function: can either be a fitness function or
a class implementing a fitness function + methods to override
the default ones: create_offspring, mutate_population, initialize_population
a class implementing a fitness function + methods to override
the default ones: create_offspring, mutate_population, initialize_population
:param n_genes: number of genes (variables) to have in each chromosome
:param max_gen: maximum number of generations to perform the optimization
:param pop_size: population size
Expand All @@ -41,7 +42,8 @@
:param plot_results: whether to plot results of the run at the end
:param variables_limits: limits for each variable [(x1_min, x1_max), (x2_min, x2_max), ...].
If only one tuple is provided, then it is assumed the same for every variable
:param problem_type: whether problem is of float or integer type
:param variables_type: the type of each variable. Can be supplied for all variables or as tuple
for each individual variable.
:param fitness_tolerance: optional. (a, b) tuple consisting of the tolerance on the
change in the best fitness, and the number of generations the condition
holds true. If the best fitness does not change by a value of (a) for a specified
Expand Down Expand Up @@ -75,7 +77,17 @@
variables_limits = [variables_limits for _ in range(n_genes)]

self.variables_limits = variables_limits
self.problem_type = problem_type

if not variables_type:
self.variables_type = [float] * n_genes
elif variables_type in [float, int]:
self.variables_type = [variables_type] * n_genes
elif isinstance(variables_type, (tuple, list, np.ndarray)):
if len(variables_type) != n_genes:
raise InvalidInput("`variables_type` must have the same dimension as `n_genes`")
self.variables_type = variables_type
else:
raise InvalidInput("`variables_type` must be either `float`, `int`, or a tuple of those for each gene")

self.beta = 0.5

Expand All @@ -91,7 +103,7 @@
population = np.empty(shape=(self.pop_size, self.n_genes))

for i, variable_limits in enumerate(self.variables_limits):
if self.problem_type == float:
if self.variables_type[i] == float:
population[:, i] = np.random.uniform(
variable_limits[0], variable_limits[1], size=self.pop_size
)
Expand Down Expand Up @@ -141,15 +153,18 @@

crossover_pt = crossover_pt[0]

variable_limits = self.variables_limits[crossover_pt]

beta = np.random.rand(1)[0] if offspring_number == "first" else self.beta

if self.problem_type == float:
if self.variables_type[crossover_pt] == float:
p_new = first_parent[crossover_pt] - beta * (
first_parent[crossover_pt] - sec_parent[crossover_pt]
)
else:
variable_limits = self.variables_limits[crossover_pt]

if not variable_limits[0] <= p_new <= variable_limits[1]:
p_new = np.random.uniform(variable_limits[0], variable_limits[1] + 1)

Check warning on line 166 in geneal/genetic_algorithms/_continuous.py

View check run for this annotation

Codecov / codecov/patch

geneal/genetic_algorithms/_continuous.py#L166

Added line #L166 was not covered by tests
else:
p_new = first_parent[crossover_pt] - np.round(
beta * (first_parent[crossover_pt] - sec_parent[crossover_pt])
)
Expand Down
4 changes: 2 additions & 2 deletions tests/applications/test_travelling_salesman_problem_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

class TestTravellingSalesmanProblemSolver:
@pytest.mark.parametrize(
"problem_type, expected_result",
"variables_type, expected_result",
[
pytest.param(
int,
Expand All @@ -34,7 +34,7 @@ class TestTravellingSalesmanProblemSolver:
),
],
)
def test_initialize_population(self, problem_type, expected_result):
def test_initialize_population(self, variables_type, expected_result):

pop_size = 5
n_genes = len(G.nodes)
Expand Down
123 changes: 105 additions & 18 deletions tests/genetic_algorithms/test_continuous_genetic_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,80 @@
fitness_functions_continuous,
)
from geneal.genetic_algorithms import ContinuousGenAlgSolver
from geneal.utils.exceptions import InvalidInput
from tests.mock_fixtures.mock_fixtures import mock_matplotlib, mock_logging


class TestContinuousGenAlgSolver:

@pytest.mark.parametrize(
"variables_type",
[
pytest.param(
(int, int, int),
id="variables_type-wrong_dimensions",
),
pytest.param(
"variables_type",
id="variables_type-invalid",
)
],
)
def test_invalid_input(self, variables_type):
with pytest.raises(Exception) as excinfo:
continuous_solver = ContinuousGenAlgSolver(
fitness_function=lambda x: x.sum(),
n_genes=4,
pop_size=5,
variables_type=variables_type,
random_state=42,
)

assert excinfo.type == InvalidInput

@pytest.mark.parametrize(
"problem_type, expected_result",
"variables_type,variable_limits",
[
pytest.param(
None,
[-10, 10],
id="variables_type-None",
),
pytest.param(
[float, float, float],
[-10, 10],
id="variables_type-correct_tuple",
),
pytest.param(
None,
None,
id="variable_limits-None",
),
],
)
def test_valid_input(self, variables_type, variable_limits):

n_genes = 3

continuous_solver = ContinuousGenAlgSolver(
fitness_function=lambda x: x.sum(),
n_genes=n_genes,
pop_size=5,
variables_type=variables_type,
variables_limits=variable_limits,
random_state=42,
)

assert continuous_solver.variables_type == [float] * n_genes

if variable_limits is not None:
assert continuous_solver.variables_limits == [variable_limits] * n_genes
else:
min_max = np.iinfo(np.int64)
assert continuous_solver.variables_limits == [(min_max.min, min_max.max)] * n_genes

@pytest.mark.parametrize(
"variables_type, expected_result",
[
pytest.param(
int,
Expand All @@ -24,7 +92,7 @@ class TestContinuousGenAlgSolver:
[-3.0, 0.0, 10.0, 10.0],
]
),
id="problem_type=int",
id="variables_type=int",
),
pytest.param(
float,
Expand All @@ -37,17 +105,17 @@ class TestContinuousGenAlgSolver:
[-6.87962719, 4.16145156, -6.36350066, -4.1754172],
]
),
id="problem_type=float",
id="variables_type=float",
),
],
)
def test_initialize_population(self, problem_type, expected_result):
def test_initialize_population(self, variables_type, expected_result):

continuous_solver = ContinuousGenAlgSolver(
fitness_function=lambda x: x.sum(),
n_genes=4,
pop_size=5,
problem_type=problem_type,
variables_type=variables_type,
random_state=42,
)

Expand All @@ -56,29 +124,28 @@ def test_initialize_population(self, problem_type, expected_result):
assert np.allclose(population, expected_result, rtol=1e-05)

@pytest.mark.parametrize(
"variables_limits, problem_type",
"variables_limits, variables_type",
[
pytest.param(
(-20, 20),
int,
id="problem_type=int | variables_limits=same for all genes",
id="variables_type=int | variables_limits=same for all genes",
),
pytest.param(
[(0, 10), (-10, 5), (-100, 100), (5, 20)],
float,
id="problem_type=float | variables_limits=different for all genes",
id="variables_type=float | variables_limits=different for all genes",
),
],
)
def test_initialize_population_variable_limits(
self, variables_limits, problem_type
self, variables_limits, variables_type
):

continuous_solver = ContinuousGenAlgSolver(
fitness_function=lambda x: x.sum(),
n_genes=4,
pop_size=5,
problem_type=problem_type,
variables_type=variables_type,
variables_limits=variables_limits,
random_state=42,
)
Expand All @@ -90,34 +157,51 @@ def test_initialize_population_variable_limits(
assert population[:, i].max() < continuous_solver.variables_limits[i][1]

@pytest.mark.parametrize(
"crossover_pt, expected_first_offspring, expected_second_offspring",
"variables_type,crossover_pt,expected_first_offspring,expected_second_offspring",
[
pytest.param(
float,
np.array([0]),
np.array([1.37454012, 2, -4, 0]),
np.array([1.62545988, -3.0, 5.0, 0.0]),
id="crossover_point=0",
id="float-crossover_point=0",
),
pytest.param(
float,
np.array([2]),
np.array([1, -3, 1.62913893, 0]),
np.array([[2.0, 2.0, -0.62913893, 0.0]]),
id="crossover_point=4",
id="float-crossover_point=4",
),
pytest.param(
float,
np.array([3]),
np.array([1.0, -3.0, 5.0, 0.0]),
np.array([2.0, 2.0, -4.0, 0.0]),
id="crossover_point=9",
id="float-crossover_point=9",
),
pytest.param(
int,
np.array([0]),
[1, 2, - 4, 0],
[2, -3, 5, 0],
id="int-crossover_point=0",
),
pytest.param(
int,
np.array([2]),
[1, - 3, 2, 0],
[2, 2, - 1, 0],
id="int-crossover_point=4",
),
],
)
def test_create_offspring(
self, crossover_pt, expected_first_offspring, expected_second_offspring
def test_create_offspring_float(
self, variables_type, crossover_pt, expected_first_offspring, expected_second_offspring
):

continuous_solver = ContinuousGenAlgSolver(
fitness_function=lambda x: x.sum(), n_genes=4, pop_size=5, random_state=42
fitness_function=lambda x: x.sum(), n_genes=4, pop_size=5, random_state=42, variables_type=variables_type
)

first_parent = np.array([1, -3, 5, 0])
Expand All @@ -131,6 +215,9 @@ def test_create_offspring(
sec_parent, first_parent, crossover_pt, "second"
)

print(first_offspring)
print(second_offspring)

assert np.allclose(first_offspring, expected_first_offspring, rtol=1e-5)
assert np.allclose(second_offspring, expected_second_offspring, rtol=1e-5)

Expand Down
Loading