Skip to content

Commit

Permalink
Refactor all crossover classes (onepoint, tsp, etc.)
Browse files Browse the repository at this point in the history
 - Add type hints for all methods/functions
 - Refactor file and method/function docstrings
 - General code style improvements
  • Loading branch information
knakamura13 committed Aug 4, 2024
1 parent c9f7652 commit b456fcd
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 107 deletions.
2 changes: 1 addition & 1 deletion mlrose_hiive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .algorithms.gd import gradient_descent
from .algorithms.mimic import mimic
from .algorithms.decay import GeomDecay, ArithDecay, ExpDecay, CustomSchedule
from .algorithms.crossovers import OnePointCrossOver, UniformCrossOver, TSPCrossOver
from .algorithms.crossovers import OnePointCrossover, UniformCrossover, TSPCrossover
from .algorithms.mutators import ChangeOneMutator, DiscreteMutator, SwapMutator, ShiftOneMutator

from .fitness import OneMax, FlipFlop, FourPeaks, SixPeaks, ContinuousPeaks, Knapsack, TravellingSales, Queens, MaxKColor, CustomFitness
Expand Down
2 changes: 1 addition & 1 deletion mlrose_hiive/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
from .gd import gradient_descent
from .mimic import mimic

from .crossovers import UniformCrossOver, TSPCrossOver, OnePointCrossOver
from .crossovers import UniformCrossover, TSPCrossover, OnePointCrossover
from .decay import ArithDecay, CustomSchedule, ExpDecay, GeomDecay
from .mutators import ChangeOneMutator, DiscreteMutator, ShiftOneMutator, SwapMutator
6 changes: 3 additions & 3 deletions mlrose_hiive/algorithms/crossovers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
# Author: Genevieve Hayes
# License: BSD 3 clause

from .uniform_crossover import UniformCrossOver
from .tsp_crossover import TSPCrossOver
from .one_point_crossover import OnePointCrossOver
from .tsp_crossover import TSPCrossover
from .uniform_crossover import UniformCrossover
from .one_point_crossover import OnePointCrossover
70 changes: 61 additions & 9 deletions mlrose_hiive/algorithms/crossovers/_crossover_base.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
"""Crossover implementations for GA."""
"""Crossover implementations for Genetic Algorithms (GA).
# Author: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
# License: BSD 3 clause
This module defines a base class for crossover operations used in genetic algorithms,
detailing how two parent solutions can be combined to create offspring.
Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
License: BSD 3 Clause
"""

from abc import ABC, abstractmethod
from typing import Any


class _CrossoverBase(ABC):
"""
Base class for crossover operations in a genetic algorithm.
Provides a structured way to define crossover behavior in genetic algorithms.
It should be subclassed to implement specific crossover strategies.
Parameters
----------
optimization_problem : Any
An instance of the optimization problem related to the genetic algorithm.
This problem instance should provide necessary properties like 'length'
that might be needed for the crossover operation.
class _CrossOverBase(ABC):
"""Base class for crossover operations in a genetic algorithm."""
Attributes
----------
optimization_problem : Any
The optimization problem instance.
chromosome_length : int
Length of the chromosome, typically derived from the optimization problem's
'length' property.
"""

def __init__(self, opt_prob):
def __init__(self, optimization_problem: Any) -> None:
"""
Initialize the CrossoverBase with the given optimization problem.
Parameters
----------
optimization_problem : Any
An instance of the optimization problem related to the GA.
"""
super().__init__()
self._opt_prob = opt_prob
self._length = opt_prob.length
self.optimization_problem = optimization_problem
self.chromosome_length: int = optimization_problem.length

@abstractmethod
def mate(self, p1, p2):
def mate(self, parent1: Any, parent2: Any) -> Any:
"""
Perform the crossover (mating) between two parents to produce offspring.
This method must be implemented by subclasses to define specific crossover
behavior based on the genetics of the parents.
Parameters
----------
parent1 : Any
The first parent participating in the crossover.
parent2 : Any
The second parent participating in the crossover.
Returns
-------
Any
The offspring resulting from the crossover. The type of this result
can vary depending on the specific GA implementation.
"""
pass
64 changes: 53 additions & 11 deletions mlrose_hiive/algorithms/crossovers/one_point_crossover.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
"""Crossover implementations for GA."""
"""One Point Crossover implementation for Genetic Algorithms (GA).
# Author: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
# License: BSD 3 clause
This module defines a one-point crossover operation used in genetic algorithms,
where a single crossover point is chosen randomly to combine two parent solutions.
Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
License: BSD 3 Clause
"""

import numpy as np
from typing import Any, Sequence

from mlrose_hiive.algorithms.crossovers._crossover_base import _CrossoverBase


class OnePointCrossover(_CrossoverBase):
"""
One-point crossover for genetic algorithms.
This class implements a simple one-point crossover, where a single crossover
point on the parent chromosomes is chosen randomly, and the genetic information
is exchanged to create a new offspring.
Inherits from:
_CrossoverBase : Abstract base class for crossover operations.
"""

def __init__(self, optimization_problem: Any) -> None:
"""
Initialize the OnePointCrossover with the given optimization problem.
Parameters
----------
optimization_problem : Any
An instance of the optimization problem related to the genetic algorithm.
"""
super().__init__(optimization_problem)

from mlrose_hiive.algorithms.crossovers._crossover_base import _CrossOverBase
def mate(self, parent1: Sequence[float], parent2: Sequence[float]) -> np.ndarray:
"""
Perform the one-point crossover between two parent sequences to produce offspring.
A single crossover point is selected randomly from the chromosome. All genes before
that point are copied from the first parent and the rest from the second parent.
class OnePointCrossOver(_CrossOverBase):
def __init__(self, opt_prob):
super().__init__(opt_prob)
Parameters
----------
parent1 : Sequence[float]
The first parent chromosome sequence.
parent2 : Sequence[float]
The second parent chromosome sequence.
def mate(self, p1, p2):
n = 1 + np.random.randint(self._length-1)
child = np.array([*p1[:n], *p2[n:]])
return child
Returns
-------
np.ndarray
The offspring chromosome resulting from the crossover.
"""
crossover_point = 1 + np.random.randint(self.chromosome_length - 1)
offspring = np.array([*parent1[:crossover_point], *parent2[crossover_point:]])
return offspring
180 changes: 129 additions & 51 deletions mlrose_hiive/algorithms/crossovers/tsp_crossover.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,147 @@
"""TSP Crossover implementation for GA."""
"""TSP Crossover implementation for Genetic Algorithms (GA).
# Author: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
# License: BSD 3 clause
This module defines a TSP-specific crossover operation used in genetic algorithms,
which handles the mating of parent solutions to produce offspring that respect the TSP
constraints.
Authors: Genevieve Hayes (modified by Andrew Rollings, Kyle Nakamura)
License: BSD 3 Clause
"""

import numpy as np
from typing import Any, Sequence

from mlrose_hiive.algorithms.crossovers._crossover_base import _CrossoverBase


class TSPCrossover(_CrossoverBase):
"""
Crossover operation tailored for the Traveling Salesman Problem (TSP) in genetic algorithms.
Implements specific crossover techniques that ensure valid TSP routes in the offspring.
The crossover handles distinct city sequences without repetitions and uses specialized
logic to combine parental genes.
from mlrose_hiive.algorithms.crossovers._crossover_base import _CrossOverBase
Inherits from:
_CrossoverBase : Abstract base class for crossover operations.
"""

def __init__(self, optimization_problem: Any) -> None:
"""
Initialize the TSPCrossover with the given optimization problem.
class TSPCrossOver(_CrossOverBase):
Parameters
----------
optimization_problem : Any
An instance of the optimization problem related to the genetic algorithm.
"""
super().__init__(optimization_problem)

def __init__(self, opt_prob):
super().__init__(opt_prob)
def mate(self, parent1: Sequence[int], parent2: Sequence[int]) -> np.ndarray:
"""
Perform the crossover (mating) between two parent sequences to produce offspring.
def mate(self, p1, p2):
return self._mate_fill(p1, p2)
Chooses between two internal methods to generate offspring based on TSP-specific
constraints and optimizations.
def _mate_fill(self, p1, p2):
if self._length > 1:
n = 1 + np.random.randint(self._length - 1)
child = np.array([0] * self._length)
child[:n] = p1[:n]
Parameters
----------
parent1 : Sequence[int]
The first parent representing a TSP route.
parent2 : Sequence[int]
The second parent representing a TSP route.
unvisited = [node for node in p2 if node not in p1[:n]]
Returns
-------
np.ndarray
The offspring representing a new TSP route.
"""
return self._mate_fill(parent1, parent2)

def _mate_fill(self, parent1: Sequence[int], parent2: Sequence[int]) -> np.ndarray:
"""
Perform a fill-based crossover using a segment of the first parent and filling
the rest with non-repeated cities from the second parent.
Parameters
----------
parent1 : Sequence[int]
The first parent representing a TSP route.
parent2 : Sequence[int]
The second parent representing a TSP route.
Returns
-------
np.ndarray
The offspring TSP route.
"""
if self.chromosome_length > 1:
n = 1 + np.random.randint(self.chromosome_length - 1)
child = np.array([0] * self.chromosome_length)
child[:n] = parent1[:n]
unvisited = [city for city in parent2 if city not in parent1[:n]]
child[n:] = unvisited
elif np.random.randint(2) == 0:
child = np.copy(p1)
else:
child = np.copy(p2)
child = np.copy(parent1 if np.random.randint(2) == 0 else parent2)
return child

def _mate_traverse(self, parent_1, parent_2):
if self._length > 1:
next_a = np.append(parent_1[1:], parent_1[-1])
next_b = np.append(parent_2[1:], parent_2[-1])

visited = [False] * self._length
child = np.array([0] * self._length)

v = np.random.randint(len(parent_1))
child[0] = v
visited[v] = True
for i in range(1, len(child)):
cur = child[i-1]
na = next_a[cur]
nb = next_b[cur]
va = visited[na]
vb = visited[nb]
if va and not vb:
nx = nb
elif not va and vb:
nx = na
elif not va and not vb:
fa = self._opt_prob.fitness_fn.calculate_fitness([cur, na])
fb = self._opt_prob.fitness_fn.calculate_fitness([cur, nb])
nx = nb if fa > fb else na # opposite because they're distance and smaller is better
def _mate_traverse(self, parent1: Sequence[int], parent2: Sequence[int]) -> np.ndarray:
"""
Perform a traversal-based crossover using city adjacency considerations from
both parents to construct a viable TSP route.
The method determines the next city to visit based on the adjacency in both
parents' routes, considering previously visited cities and selecting based
on fitness values where applicable.
Parameters
----------
parent1 : Sequence[int]
The first parent representing a TSP route.
parent2 : Sequence[int]
The second parent representing a TSP route.
Returns
-------
np.ndarray
The offspring TSP route.
"""
if self.chromosome_length > 1:
next_city_parent1 = np.append(parent1[1:], parent1[0])
next_city_parent2 = np.append(parent2[1:], parent2[0])

visited_cities = [False] * self.chromosome_length
offspring_route = np.array([0] * self.chromosome_length)

starting_city = np.random.randint(len(parent1))
offspring_route[0] = starting_city
visited_cities[starting_city] = True

for index in range(1, len(offspring_route)):
current_city = offspring_route[index - 1]
next_city1 = next_city_parent1[current_city]
next_city2 = next_city_parent2[current_city]

visited_city1 = visited_cities[next_city1]
visited_city2 = visited_cities[next_city2]

if visited_city1 and not visited_city2:
next_city = next_city2
elif not visited_city1 and visited_city2:
next_city = next_city1
elif not visited_city1 and not visited_city2:
fitness1 = self.optimization_problem.fitness_fn.calculate_fitness([current_city, next_city1])
fitness2 = self.optimization_problem.fitness_fn.calculate_fitness([current_city, next_city2])
next_city = next_city2 if fitness1 > fitness2 else next_city1 # Choose the smaller distance
else:
while True:
nx = np.random.randint(len(parent_1))
if not visited[nx]:
next_city = np.random.randint(len(parent1))
if not visited_cities[next_city]:
break
child[i] = nx
visited[nx] = True
elif np.random.randint(2) == 0:
child = np.copy(parent_1)

offspring_route[index] = next_city
visited_cities[next_city] = True
else:
child = np.copy(parent_2)
return child
offspring_route = np.copy(parent1 if np.random.randint(2) == 0 else parent2)

return offspring_route
Loading

0 comments on commit b456fcd

Please sign in to comment.