GenAlg
A flexible, high-performance genetic algorithm library written in Rust.
Overview
GenAlg is a modern, thread-safe genetic algorithm framework designed for flexibility, performance, and ease of use. It provides a robust foundation for implementing evolutionary algorithms to solve optimization problems across various domains.
Key Features
- High Performance: Optimized for speed with thread-local random number generation
- Flexible: Adaptable to a wide range of optimization problems
- Extensible: Easy to implement custom phenotypes, fitness functions, and breeding strategies
- Local Search Integration: Enhance solutions with integrated local search algorithms
- Parallel Processing: Automatic parallelization for large populations using Rayon
Installation
Add GenAlg to your Cargo.toml:
[]
= "0.1.0"
Quick Start
Here's a simple example that optimizes a function to find a target value:
use ;
// Define your phenotype (the solution representation)
// Define your fitness function
Core Concepts
Phenotype
The Phenotype trait defines the interface for types that represent individuals in the evolutionary algorithm. Implement this trait for your custom solution representation:
Challenge
The Challenge trait defines how to evaluate the fitness of phenotypes:
Evolution Options
Configure the evolution process with EvolutionOptions:
let options = new;
// Or with parallel threshold
let options = new_with_threshold;
// Or use the builder pattern
let options = builder
.num_generations
.log_level
.population_size
.num_offspring
.parallel_threshold
.build;
Breeding Strategies
GenAlg provides three built-in breeding strategies:
-
OrdinaryStrategy: A basic breeding strategy where the first parent is considered the winner of the previous generation.
-
BoundedBreedStrategy: Similar to
OrdinaryStrategybut imposes bounds on phenotypes during evolution. -
CombinatorialBreedStrategy: Specialized strategy for combinatorial optimization problems, supporting constraint handling through repair and penalties.
use ; // Create a configuration with repair and penalties let config = builder .repair_probability .max_repair_attempts .use_penalties .penalty_weight .build; // Create the strategy let mut breed_strategy = new; // Add constraints (see the Combinatorial Breeding Strategy section for details)
Selection Strategies
GenAlg provides several built-in selection strategies for choosing parents based on their fitness:
-
ElitistSelection: Selects the best individuals based on fitness scores. This strategy implements elitism, which ensures that the best solutions are preserved across generations.
use ElitistSelection; // Default: higher fitness is better, no duplicates allowed let selection = default; // For minimization problems (lower fitness is better) let selection = with_options; // Allow duplicates in selection let selection = with_duplicates; -
TournamentSelection: Randomly selects groups of individuals and picks the best from each group. This provides a balance between exploration and exploitation.
use TournamentSelection; // Tournament size determines selection pressure // Larger size = more pressure toward best individuals let selection = new; // For minimization problems let selection = with_options; -
RouletteWheelSelection: Selects individuals with probability proportional to their fitness. Also known as fitness proportionate selection.
use RouletteWheelSelection; // Default: higher fitness is better, no duplicates let selection = default; // For minimization problems let selection = with_options; -
RankBasedSelection: Selects individuals based on their rank in the population rather than their absolute fitness. This helps prevent premature convergence when there are a few individuals with much higher fitness.
use RankBasedSelection; // Default: selection pressure of 1.5, higher is better let selection = default; // Custom selection pressure (between 1.0 and 2.0) let selection = with_pressure.unwrap; // For minimization problems let selection = with_options.unwrap;
You can also implement your own selection strategy by implementing the SelectionStrategy trait:
use ;
use Debug;
Advanced Usage
Local Search Integration
GenAlg supports integrating local search algorithms to enhance the quality of solutions:
use ;
// Create a local search algorithm
let hill_climbing = new.unwrap; // 10 iterations max, 10 neighbors to evaluate
// Create a local search application strategy
let application_strategy = new;
// Create a local search manager
let local_search_manager = new;
// Create the launcher with local search
let launcher = new;
// Or enable local search during configuration
let result = launcher
.configure
.with_local_search // Enable local search
.run
.unwrap;
Available local search algorithms:
-
HillClimbing: A random restart hill climbing algorithm that iteratively makes small improvements.
use HillClimbing; // Basic hill climbing with default neighbors let hill_climbing = new.unwrap; -
SimulatedAnnealing: Allows accepting worse solutions with decreasing probability over time.
use SimulatedAnnealing; // Parameters: max iterations, initial temperature, cooling rate let simulated_annealing = new.unwrap; -
TabuSearch: Prevents revisiting recently explored solutions.
use TabuSearch; // Parameters: max iterations, max neighbors, tabu list size let tabu_search = new.unwrap; -
HybridLocalSearch: Combines multiple local search algorithms.
use ; let mut hybrid = new; hybrid.add_algorithm .add_algorithm;
Local search application strategies:
-
AllIndividualsStrategy: Applies local search to all individuals.
use AllIndividualsStrategy; let strategy = new; -
TopNStrategy: Applies local search to the top N individuals.
use TopNStrategy; // Apply to top 3 individuals (maximizing) let strategy = new_maximizing; // Apply to top 3 individuals (minimizing) let strategy = new_minimizing; -
TopPercentStrategy: Applies local search to a percentage of top individuals.
use TopPercentStrategy; // Apply to top 20% of individuals (maximizing) let strategy = new_maximizing.unwrap; -
ProbabilisticStrategy: Applies local search with a certain probability.
use ProbabilisticStrategy; // 30% chance of applying local search to each individual let strategy = new.unwrap;
Bounded Evolution
For problems with constraints, use the BoundedBreedStrategy with the Magnitude trait:
use ;
// Then use BoundedBreedStrategy
let strategy = new; // 1000 max development attempts
Implementing Custom Breeding Strategies
One of the key design features of GenAlg is the ability to implement your own custom breeding strategies. This allows you to tailor the evolutionary process to your specific problem domain.
To create a custom breeding strategy, implement the BreedStrategy trait:
use ;
Combinatorial Breeding Strategy
GenAlg provides a specialized breeding strategy for combinatorial optimization problems through the CombinatorialBreedStrategy. This strategy is designed to handle discrete solution spaces with complex constraints effectively.
What are Combinatorial Optimization Problems?
Combinatorial optimization problems involve finding an optimal arrangement, grouping, ordering, or selection of discrete objects. Examples include:
- Assignment Problems: Assigning resources to tasks
- Routing Problems: Finding optimal paths (e.g., Traveling Salesman Problem)
- Scheduling Problems: Arranging tasks in time with dependencies
- Packing Problems: Fitting items into containers with capacity constraints
Using CombinatorialBreedStrategy
The CombinatorialBreedStrategy offers powerful constraint-handling capabilities:
use ;
// Create a configuration
let config = builder
.repair_probability
.max_repair_attempts
.use_penalties
.penalty_weight
.build;
// Create the breeding strategy
let mut breed_strategy = new;
// Add constraints
breed_strategy.add_constraint;
Configuration Options
The CombinatorialBreedStrategy can be configured with the following options:
// Default configuration
let default_config = default;
// Custom configuration with builder pattern
let config = builder
.repair_probability // Probability of attempting repair (default: 0.5)
.max_repair_attempts // Maximum repair attempts (default: 10)
.use_penalties // Whether to use penalties (default: false)
.penalty_weight // Weight applied to penalties (default: 1.0)
.build;
Constraint Handling Approaches
The strategy supports two complementary approaches to handling constraints:
-
Repair-based Approach: Invalid solutions are repaired during breeding
- Controlled by
repair_probabilityandmax_repair_attempts - Attempts to fix invalid solutions by applying constraint repair operations
- Controlled by
-
Penalty-based Approach: Invalid solutions are penalized during fitness evaluation
- Controlled by
use_penaltiesandpenalty_weight - Reduces the fitness of solutions that violate constraints
- Controlled by
These approaches can be used separately or in combination:
// Repair-only approach
let repair_config = builder
.repair_probability
.max_repair_attempts
.use_penalties
.build;
// Penalty-only approach
let penalty_config = builder
.repair_probability
.use_penalties
.penalty_weight
.build;
// Combined approach
let combined_config = builder
.repair_probability
.max_repair_attempts
.use_penalties
.penalty_weight
.build;
Adding Constraints
To add constraints to your breeding strategy:
// Create a constraint
;
// Add the constraint to the strategy
let mut strategy = new;
strategy.add_constraint;
Built-in Combinatorial Constraints
GenAlg provides several built-in constraints for common combinatorial problems:
-
UniqueElementsConstraint: Ensures all elements in a collection are unique
use UniqueElementsConstraint; // Create a constraint that ensures a Vec<usize> contains unique values let unique_constraint = new.unwrap; breed_strategy.add_constraint; -
CompleteAssignmentConstraint: Ensures all required keys have a value
use CompleteAssignmentConstraint; use ; // Required keys that must be assigned let required_keys: = .collect; // Create a constraint that ensures all tasks are assigned let assignment_constraint = new.unwrap; breed_strategy.add_constraint; -
CapacityConstraint: Ensures bins don't exceed their capacity
use CapacityConstraint; // Create a constraint that ensures bins don't exceed capacity let capacity_constraint = new.unwrap; breed_strategy.add_constraint; -
DependencyConstraint: Ensures dependencies between elements are respected
use DependencyConstraint; // Define task dependencies (before, after) pairs let dependencies = vec!; // Create a constraint that ensures task dependencies are respected let dependency_constraint = new.unwrap; breed_strategy.add_constraint;
Using Penalty-Adjusted Challenges
When using the penalty-based approach, you need to create a penalty-adjusted challenge:
use PenaltyAdjustedChallenge;
// Original challenge
let original_challenge = new;
// Create a penalty-adjusted challenge
let penalty_challenge = breed_strategy.create_penalty_challenge;
// Use the penalty-adjusted challenge in the evolution launcher
let launcher = new;
Complete Example: Traveling Salesman Problem
Here's a complete example showing how to use the combinatorial breeding strategy for a Traveling Salesman Problem:
use ;
use Debug;
// Define a phenotype representing a route
// Fitness function calculating total distance
Fitness Caching
GenAlg provides built-in caching functionality to improve performance by avoiding redundant fitness evaluations. This is particularly useful in scenarios with:
- Expensive fitness evaluations
- Frequent occurrences of similar phenotypes
- Large populations or many generations
How Caching Works
Caching in GenAlg is implemented as a wrapper around your challenge:
- When a phenotype is evaluated, the system first checks if its fitness value is in the cache
- If found, the cached value is returned without recalculating
- If not found, the actual fitness function is called and the result is stored in the cache
Enabling Caching
There are two ways to enable caching:
- Via Evolution Options (simplest):
let options = builder
.num_generations
.population_size
.use_caching // Enable caching
.cache_type // Choose cache type
.build;
let result = launcher
.configure
.run
.unwrap;
- Direct Caching Challenge Creation (for more control):
use CachingChallenge;
// Original challenge
let challenge = MyChallenge ;
// With global cache (thread-safe, shared across all threads)
let global_cached_challenge = challenge.with_global_cache;
// Or with thread-local cache (separate cache per thread)
let thread_local_cached_challenge = challenge.with_thread_local_cache;
// Or at runtime based on configuration
let cache_type = Global; // or CacheType::ThreadLocal
let cached_challenge = challenge.with_cache;
Implementing the CacheKey Trait
For caching to work, your phenotype must implement the CacheKey trait:
use CacheKey;
Guidelines for implementing CacheKey:
- The key must uniquely identify phenotypes that would have the same fitness score
- The key generation should be computationally efficient
- For floating-point values, consider rounding to handle precision issues
- For complex phenotypes, only include the parts that affect fitness
Cache Types
GenAlg provides two cache implementations:
-
Global Cache (
CacheType::Global):- Single cache shared across all threads
- Protected by a mutex
- Maximizes cache reuse
- Good for problems with low thread contention or single-threaded use
-
Thread-Local Cache (
CacheType::ThreadLocal):- Separate cache for each thread
- No mutex contention
- Better performance for highly parallel workloads
- May have some redundant evaluations across threads
Performance Considerations
- Cache Growth: The cache grows unbounded by default. For long-running evolutions, consider clearing it periodically.
- Thread Contention: For highly parallel workloads, thread-local caching may outperform global caching due to reduced mutex contention.
- Key Generation: Ensure the
cache_key()method is efficient, as it's called for every fitness evaluation. - Memory Usage: Monitor memory usage if caching a very large number of phenotypes.
Example: Advanced Caching
use ;
// Define a phenotype with CacheKey implementation
// Expensive challenge that benefits from caching
;
// Run evolution with caching
Serialization Support
GenAlg provides optional serialization support using serde.
Enabling Serialization
To use serialization, you need to enable the serde feature in your Cargo.toml:
[]
= { = "0.1.0", = ["serde"] }
= { = "1.0", = ["derive"] }
= "1.0" # Or any other serde format you prefer
Serializable Types
The following GenAlg types support serialization when the feature is enabled:
EvolutionResultEvolutionOptionsLogLevelCacheTypeConstraintViolationConstraintError
Making Your Phenotypes Serializable
To make your phenotypes serializable, implement the Serialize and Deserialize traits from serde:
use Phenotype;
use ;
// The SerializablePhenotype trait is automatically implemented
// for any type that implements both Phenotype and the serde traits
Parallel Processing
GenAlg automatically uses parallel processing for fitness evaluation and breeding when the population size exceeds the parallel threshold. Configure this in your EvolutionOptions:
let mut options = default;
options.set_parallel_threshold; // Use parallel processing when population >= 500
Thread-Local Random Number Generation
For optimal performance in parallel contexts, GenAlg provides thread-local random number generation:
use ThreadLocalRng;
// Generate a random number in a range
let value = gen_range;
// Generate multiple random numbers
let numbers = fetch_uniform;
The Phenotype trait includes a default implementation of mutate_thread_local() that uses this feature, but you can override it for better performance:
Logging with Tracing
GenAlg uses the tracing crate for structured logging. To enable logging in your application, you need to set up a tracing subscriber:
use ;
// Initialize the default subscriber with an environment filter
// Or with a specific level