Genetic Algorithm Framework in JavaScript
This challenge asks you to build a foundational genetic algorithm (GA) framework in JavaScript. Genetic algorithms are powerful optimization techniques inspired by natural selection, useful for solving complex problems where finding an exact solution is difficult or impossible. Your framework should be flexible enough to be adapted to various optimization scenarios.
Problem Description
You are tasked with creating a reusable JavaScript framework for implementing genetic algorithms. The framework should include core components like population management, fitness evaluation, selection, crossover, and mutation. The framework should not be tied to a specific problem; it should be a general-purpose tool that can be configured to solve different optimization problems.
What needs to be achieved:
- Representation: Define a way to represent individuals (potential solutions) within the population. For this challenge, individuals will be represented as arrays of numbers (floating-point values).
- Population Management: Implement functions to initialize a population of individuals randomly and to manage the population throughout the GA process.
- Fitness Evaluation: Provide a placeholder for a fitness function. This function will be provided by the user and will evaluate the quality of each individual in the population. The framework should not implement the fitness function itself, but rather accept it as an argument.
- Selection: Implement a selection mechanism (e.g., tournament selection, roulette wheel selection) to choose individuals for reproduction. Tournament selection is recommended for simplicity.
- Crossover: Implement a crossover operator (e.g., single-point crossover, uniform crossover) to combine genetic material from two parent individuals to create offspring. Single-point crossover is recommended.
- Mutation: Implement a mutation operator to introduce random changes into individuals.
- Iteration: Provide a function to run the GA for a specified number of generations.
Key Requirements:
- The framework should be modular and extensible.
- The fitness function should be passed as a parameter.
- The framework should provide clear methods for initializing, evaluating, selecting, crossing over, and mutating individuals.
- The framework should return the best individual found after a specified number of generations.
Expected Behavior:
The framework should take the following as input:
populationSize: The number of individuals in the population.chromosomeLength: The length of the individual's chromosome (array of numbers).fitnessFunction: A function that takes an individual (array of numbers) as input and returns a fitness score (a number). Higher scores indicate better fitness.crossoverRate: The probability of crossover occurring between two parents (a number between 0 and 1).mutationRate: The probability of a gene mutating in an individual (a number between 0 and 1).numGenerations: The number of generations to run the GA for.tournamentSize: The number of individuals participating in each tournament selection.
The framework should return the best individual found after numGenerations generations.
Edge Cases to Consider:
- Invalid input parameters (e.g., negative population size, crossover rate outside of [0, 1]). Handle these gracefully, potentially throwing errors or returning a default value.
- Fitness function returning NaN or Infinity. The framework should handle these cases without crashing.
- Mutation rate too high, leading to rapid degradation of the population.
- Crossover rate too high, potentially destroying beneficial genetic material.
Examples
Example 1:
Input: { populationSize: 50, chromosomeLength: 10, fitnessFunction: (individual) => individual.reduce((a, b) => a + b, 0), crossoverRate: 0.7, mutationRate: 0.01, numGenerations: 100, tournamentSize: 3 }
Output: [0.8, 0.2, 0.9, 0.1, 0.7, 0.3, 0.6, 0.4, 0.5, 0.0] (Example - actual output will vary)
Explanation: The fitness function sums the elements of the array. The GA will evolve a population of 10-element arrays to maximize their sum. The output is the best individual found after 100 generations.
Example 2:
Input: { populationSize: 20, chromosomeLength: 5, fitnessFunction: (individual) => Math.sin(individual.reduce((a, b) => a + b, 0)), crossoverRate: 0.6, mutationRate: 0.05, numGenerations: 50, tournamentSize: 2 }
Output: [0.5, 0.7, 0.2, 0.9, 0.3] (Example - actual output will vary)
Explanation: The fitness function calculates the sine of the sum of the array elements. The GA will evolve a population of 5-element arrays to maximize the sine of their sum.
Constraints
- Population Size: Must be a positive integer.
- Chromosome Length: Must be a positive integer.
- Crossover Rate: Must be a number between 0 and 1 (inclusive).
- Mutation Rate: Must be a number between 0 and 1 (inclusive).
- Number of Generations: Must be a positive integer.
- Tournament Size: Must be a positive integer, less than the population size.
- Performance: The framework should be reasonably efficient for populations of up to 100 individuals and chromosome lengths of up to 20. Optimization for extremely large populations is not required.
Notes
- Focus on creating a clean, well-documented, and extensible framework.
- Consider using JavaScript's built-in
Mathobject for random number generation. - You can choose any suitable selection, crossover, and mutation operators. Tournament selection and single-point crossover are recommended for simplicity.
- The fitness function is the only problem-specific part of the framework. The framework itself should be independent of the specific problem being solved.
- Error handling is important. Make sure your framework handles invalid inputs gracefully.
- Document your code clearly, explaining the purpose of each function and variable.
- Test your framework thoroughly with different fitness functions and parameter settings.