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
115 changes: 115 additions & 0 deletions job_shop_lib/genetic_algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# job_shop_lib/genetic_algorithm.py

import random

class GeneticAlgorithm:
"""
A class to encapsulate the logic for a Genetic Algorithm solver.
This class is unchanged from the previous version.
"""
Comment on lines +5 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current GeneticAlgorithm implementation is tightly coupled with a binary representation for individuals (e.g., random.randint(0, 1) in _initialize_population and 1 - gene in _mutation). This significantly limits its applicability. For a library named job_shop_lib, one would expect solvers that can handle permutation-based or other complex encodings typical for job shop scheduling problems.

To make the solver more generic and reusable, consider adopting a more flexible design. For example, you could allow the user to provide their own functions for initialization, crossover, and mutation as arguments to __init__. This would decouple the core GA logic from the specific problem representation.

def __init__(self, fitness_func, population_size, num_genes,
crossover_rate=0.8, mutation_rate=0.01, tournament_size=5):
self.fitness_func = fitness_func
self.population_size = population_size
self.num_genes = num_genes
self.crossover_rate = crossover_rate
self.mutation_rate = mutation_rate
self.tournament_size = tournament_size
self.population = self._initialize_population()

def _initialize_population(self):
population = []
for _ in range(self.population_size):
individual = [random.randint(0, 1) for _ in range(self.num_genes)]
population.append(individual)
return population

def _selection(self):
tournament = random.sample(self.population, self.tournament_size)
best_individual = max(tournament, key=self.fitness_func)
return best_individual

def _crossover(self, parent1, parent2):
if random.random() < self.crossover_rate:
point = random.randint(1, self.num_genes - 1)
child1 = parent1[:point] + parent2[point:]
child2 = parent2[:point] + parent1[point:]
return child1, child2
return parent1, parent2

def _mutation(self, individual):
mutated_individual = []
for gene in individual:
if random.random() < self.mutation_rate:
mutated_individual.append(1 - gene)
else:
mutated_individual.append(gene)
return mutated_individual

def solve(self, num_generations):
best_overall_individual = None
best_overall_fitness = -float('inf')

print("\n🧬 Starting Genetic Algorithm...")
for generation in range(num_generations):
fitness_scores = [self.fitness_func(ind) for ind in self.population]
best_fitness_in_gen = max(fitness_scores)

if best_fitness_in_gen > best_overall_fitness:
best_overall_fitness = best_fitness_in_gen
best_index = fitness_scores.index(best_fitness_in_gen)
best_overall_individual = self.population[best_index]

# Use \r to overwrite the line for a cleaner progress display
print(f"Generation {generation + 1}/{num_generations} | Best Fitness: {best_overall_fitness}", end='\r')

print("\n✅ Genetic Algorithm finished.")
return best_overall_individual, best_overall_fitness
Comment on lines +49 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The solve method is missing the core logic of a genetic algorithm. It correctly evaluates the initial population but never creates a new generation using selection, crossover, and mutation. As a result, the population never evolves, and the algorithm only finds the best individual from the initial random set. The _selection, _crossover, and _mutation methods are never called.

You need to implement the generational loop where a new population is created based on the fitness of the current one. This involves selecting parents, performing crossover and mutation, and replacing the old population with the new one.

    def solve(self, num_generations):
        best_overall_individual = max(self.population, key=self.fitness_func)
        best_overall_fitness = self.fitness_func(best_overall_individual)

        print("\n🧬 Starting Genetic Algorithm...")
        for generation in range(num_generations):
            # --- Create new generation ---
            new_population = []
            # Elitism: Carry over the best individual from the previous generation
            new_population.append(best_overall_individual)

            while len(new_population) < self.population_size:
                parent1 = self._selection()
                parent2 = self._selection()
                child1, child2 = self._crossover(parent1, parent2)
                new_population.append(self._mutation(child1))
                if len(new_population) < self.population_size:
                    new_population.append(self._mutation(child2))
            
            self.population = new_population

            # --- Evaluation of new population ---
            current_best_individual = max(self.population, key=self.fitness_func)
            current_best_fitness = self.fitness_func(current_best_individual)

            if current_best_fitness > best_overall_fitness:
                best_overall_fitness = current_best_fitness
                best_overall_individual = current_best_individual

            # Use \r to overwrite the line for a cleaner progress display
            print(f"Generation {generation + 1}/{num_generations} | Best Fitness: {best_overall_fitness}", end='\r')

        print("\n✅ Genetic Algorithm finished.")
        return best_overall_individual, best_overall_fitness



# --- Helper function to get validated user input ---
def get_validated_input(prompt, target_type, min_val=None, max_val=None):
"""A robust function to get and validate user input."""
while True:
user_input = input(prompt)
try:
value = target_type(user_input)
if min_val is not None and value < min_val:
print(f"Error: Value must be at least {min_val}.")
elif max_val is not None and value > max_val:
print(f"Error: Value must be no more than {max_val}.")
else:
return value
except ValueError:
print(f"Error: Invalid input. Please enter a valid {target_type.__name__}.")


# --- Dynamic Example Usage ---
if __name__ == '__main__':
# Define a simple fitness function (One-Max problem)
def one_max_fitness(individual):
return sum(individual)

print("--- ⚙️ Configure Genetic Algorithm Parameters ---")

# Get user input for GA parameters
pop_size = get_validated_input("Enter the population size (e.g., 100): ", int, min_val=10)
genes = get_validated_input("Enter the number of genes per individual (e.g., 50): ", int, min_val=1)
generations = get_validated_input("Enter the number of generations to run (e.g., 100): ", int, min_val=1)
cross_rate = get_validated_input("Enter the crossover rate (e.g., 0.8): ", float, min_val=0.0, max_val=1.0)
mut_rate = get_validated_input("Enter the mutation rate (e.g., 0.01): ", float, min_val=0.0, max_val=1.0)

# Create and run the solver with user-defined parameters
ga_solver = GeneticAlgorithm(
fitness_func=one_max_fitness,
population_size=pop_size,
num_genes=genes,
crossover_rate=cross_rate,
mutation_rate=mut_rate
)

best_solution, best_score = ga_solver.solve(generations)

print("\n--- 📊 Results ---")
print(f"Best solution found: {best_solution}")
print(f"Fitness score: {best_score} (out of a maximum possible {genes})")
Comment on lines +70 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The helper function get_validated_input and the example usage block under if __name__ == '__main__': are great for demonstrating the solver. However, including runnable example code directly within a library module is generally discouraged as it mixes library code with application code.

Your PR description mentions adding an example to docs/source/examples/run_genetic_algorithm.py. It would be best to move this code there and keep the genetic_algorithm.py file focused solely on the GeneticAlgorithm class definition.

36 changes: 36 additions & 0 deletions tests/test_genetic_algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# tests/test_genetic_algorithm.py

import unittest
# Make sure to import your GeneticAlgorithm class
from job_shop_lib.genetic_algorithm import GeneticAlgorithm

# A simple fitness function for testing purposes
def dummy_fitness(individual):
return sum(individual)

class TestGeneticAlgorithm(unittest.TestCase):

def setUp(self):
"""Set up a GA instance before each test."""
self.ga_solver = GeneticAlgorithm(
fitness_func=dummy_fitness,
population_size=10,
num_genes=8
)

def test_initialization(self):
"""Test if the population is initialized correctly."""
self.assertEqual(len(self.ga_solver.population), 10)
self.assertEqual(len(self.ga_solver.population[0]), 8)

def test_solve_returns_valid_output(self):
"""Test if the solve method runs and returns the correct format."""
num_generations = 5
best_solution, best_score = self.ga_solver.solve(num_generations)

# Check if the output has the correct length and the score is a number
self.assertEqual(len(best_solution), 8)
self.assertIsInstance(best_score, (int, float))
Comment on lines +26 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current test_solve_returns_valid_output only checks the type and shape of the return values. It doesn't verify that the genetic algorithm is actually performing optimization. This is why it fails to catch the critical bug in the solve method where the population doesn't evolve.

A more robust test would check if the fitness actually improves over generations. This would have caught the bug. Consider replacing the current test with one that asserts fitness improvement. You'll need to seed the random number generator for a deterministic result.

Here's an example of a more robust test. Note that this test will only pass after the solve method is fixed.

    def test_solve_improves_fitness_and_returns_valid_output(self):
        """Test if solve improves fitness and returns the correct format."""
        import random
        random.seed(42)

        # Use a new GA instance with parameters that are more likely to show improvement
        ga_solver = GeneticAlgorithm(
            fitness_func=dummy_fitness,
            population_size=20,
            num_genes=20,
            mutation_rate=0.1,
            crossover_rate=0.8,
            tournament_size=3
        )
        
        initial_best_fitness = max(dummy_fitness(ind) for ind in ga_solver.population)

        # Run the solver (this will only work after the `solve` method is fixed)
        best_solution, final_best_fitness = ga_solver.solve(num_generations=20)

        # Check output format
        self.assertEqual(len(best_solution), 20)
        self.assertIsInstance(final_best_fitness, (int, float))

        # Check for improvement
        self.assertGreater(final_best_fitness, initial_best_fitness, 
                         "Final fitness should be greater than initial best fitness for this seed.")


if __name__ == '__main__':
unittest.main()