feat: Add Genetic Algorithm solver with unit tests#70
feat: Add Genetic Algorithm solver with unit tests#70Akhilanandateja wants to merge 1 commit intoPabloo22:mainfrom
Conversation
There was a problem hiding this comment.
Summary of Changes
Hello @Akhilanandateja, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces a new GeneticAlgorithm solver to the metaheuristics library, expanding its suite of optimization tools. The new solver is designed for flexibility and customizability, aligning with the existing architecture of other metaheuristic implementations like Simulated Annealing. It provides a robust framework for solving various optimization problems by simulating natural selection processes.
Highlights
- New Genetic Algorithm Solver: A new
GeneticAlgorithmclass has been added, designed to manage the evolutionary process for optimization problems. - Configurable Parameters: The solver is highly configurable, allowing users to set parameters such as population size, number of genes, crossover rate, mutation rate, and tournament size.
- Core GA Operators: Key evolutionary operators are implemented, including Tournament Selection for parent selection, Single-Point Crossover for offspring creation, and Bit-Flip Mutation for maintaining genetic diversity.
- Unit Tests: Comprehensive unit tests have been added to ensure the correctness and reliability of the
GeneticAlgorithmimplementation. - Documentation: Detailed docstrings have been included throughout the code to facilitate automatic API documentation generation.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Code Review
This pull request introduces a GeneticAlgorithm solver. While the initial structure is promising, I've identified a critical issue in the core solve method, which currently does not perform any evolution, rendering the algorithm non-functional. The implementation is also tightly coupled to a binary representation, which limits its use for the intended job-shop scheduling problems. Furthermore, the unit tests are not comprehensive enough to catch the critical bug, and the library module contains example code that should be moved to a separate file. I've provided detailed feedback and code suggestions to address these points and help make the solver robust and reusable.
| 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 |
There was a problem hiding this comment.
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| class GeneticAlgorithm: | ||
| """ | ||
| A class to encapsulate the logic for a Genetic Algorithm solver. | ||
| This class is unchanged from the previous version. | ||
| """ |
There was a problem hiding this comment.
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 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)) |
There was a problem hiding this comment.
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.")| # --- 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})") No newline at end of file |
There was a problem hiding this comment.
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.
Closes #69
Description
This pull request introduces a new
GeneticAlgorithmsolver to the metaheuristics package. The implementation provides a flexible and customizable solver for optimization problems based on the principles of natural selection.The solver is designed to be consistent with other metaheuristics in the library, like Simulated Annealing.
Key Features Implemented
GeneticAlgorithmClass: A self-contained class to manage the evolutionary process.Changes Made
genetic_algorithm.pyto thejob_shop_lib/metaheuristics/directory.tests/test_genetic_algorithm.py.autosummary.docs/source/examples/run_genetic_algorithm.pyand updatedexamples.rst.This contribution enhances the library's range of available metaheuristic solvers.