kosikowski/genetic-solver
A highly generic and extensible genetic algorithm framework written in Swift. This library provides a flexible foundation for implementing genetic algorithms with customizable selection, crossover, mutation, and replacement operators.
Genetic Algorithm Overview
Genetic algorithms are optimization techniques inspired by natural selection and genetics. The algorithm follows these fundamental steps:
Initialize → Evaluate → Select → Recombine → Mutate → Replace → Test-for-End — then loop
- Initialize: Create an initial population of random individuals (potential solutions)
- Evaluate: Calculate the fitness of each individual in the population
- Select: Choose parent individuals for reproduction based on their fitness
- Recombine: Perform crossover/recombination to create offspring from selected parents
- Mutate: Introduce random changes to offspring to maintain genetic diversity
- Replace: Form the new population by replacing old individuals with offspring
- Test-for-End: Check if termination criteria are met (e.g., maximum generations, target fitness reached)
This cycle repeats until the termination condition is satisfied, gradually improving the population's fitness over generations.
Features
- Generic Design: Works with any type that conforms to
GeneticElementandFitnessEvaluatable - Customizable Operators: Full control over selection, crossover, mutation, and replacement strategies
- Protocol-Based Design: Uses
GeneticOperatorsprotocol for clean separation of concerns - Default Implementations: Built-in operators for common genetic algorithm patterns
- Type Safety: Leverages Swift's type system for compile-time safety
- Extensible: Easy to extend with custom operators and termination conditions
- State Tracking: Monitor current population and generation during execution
- Incremental Execution: Step-by-step execution with the
step()method
Installation
Swift Package Manager
Add the following dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/Kosikowski/genetic-solver.git", from: "1.0.0")
]Or add it to your Xcode project:
- File → Add Package Dependencies
- Enter the repository URL
- Select the version you want to use
Quick Start
1. Define Your Individual
First, create a type that represents an individual in your genetic algorithm:
struct MyIndividual: GeneticElement, FitnessEvaluatable {
var genes: [Int]
func fitness() -> Double {
// Calculate fitness based on your problem
return Double(genes.reduce(0, +))
}
}2. Implement Genetic Operators
You can implement operators individually or create a GeneticOperators conforming type:
Option A: Individual Operators
// Selection: Tournament selection
let selection: SelectionOperator<MyIndividual> = { population in
func selectOne() -> MyIndividual {
let candidates = (0..<3).map { _ in population.randomElement()! }
return candidates.max { $0.fitness() < $1.fitness() }!
}
return (selectOne(), selectOne())
}
// Crossover: One-point crossover
let crossover: CrossoverOperator<MyIndividual> = { parent1, parent2 in
let point = Int.random(in: 0..<parent1.genes.count)
let child1 = MyIndividual(
genes: Array(parent1.genes[..<point]) + Array(parent2.genes[point...])
)
let child2 = MyIndividual(
genes: Array(parent2.genes[..<point]) + Array(parent1.genes[point...])
)
return [child1, child2]
}
// Mutation: Random gene mutation
let mutation: MutationOperator<MyIndividual> = { individual in
var mutant = individual
let geneIndex = Int.random(in: 0..<mutant.genes.count)
mutant.genes[geneIndex] = Int.random(in: 0...100)
return mutant
}Option B: Protocol-Based Approach
struct MyGeneticOperators: GeneticOperators {
typealias Element = MyIndividual
static func selectionOperator(population: [Element]) -> (Element, Element) {
// Use default tournament selection
let candidates = (0..<3).map { _ in population.randomElement()! }
let best = candidates.max { $0.fitness() < $1.fitness() }!
return (best, best)
}
static func crossoverOperator(parent1: Element, parent2: Element) -> [Element] {
let point = Int.random(in: 0..<parent1.genes.count)
let child1 = MyIndividual(
genes: Array(parent1.genes[..<point]) + Array(parent2.genes[point...])
)
let child2 = MyIndividual(
genes: Array(parent2.genes[..<point]) + Array(parent1.genes[point...])
)
return [child1, child2]
}
static func mutationOperator(element: Element) -> Element {
var mutant = element
let geneIndex = Int.random(in: 0..<mutant.genes.count)
mutant.genes[geneIndex] = Int.random(in: 0...100)
return mutant
}
static func replacementOperator(old: [Element], new: [Element]) -> [Element] {
return new // Generational replacement
}
static func fixedGenerationTermination(maxGenerations: Int) -> TerminationCheck<Element> {
return { generation, _ in generation >= maxGenerations }
}
static func newElement() -> Element {
return MyIndividual(genes: (0..<10).map { _ in Int.random(in: 0...100) })
}
}3. Create and Run the Solver
Using Individual Operators
var solver = GeneticSolver<MyIndividual>(
populationSize: 50,
crossoverRate: 0.8,
mutationRate: 0.1,
selectionOperator: selection,
crossoverOperator: crossover,
mutationOperator: mutation,
replacementOperator: { _, new in new }, // Generational replacement
terminationCheck: { generation, population in
generation >= 100 || population.allSatisfy { $0.fitness() > 0.95 }
},
newElement: {
MyIndividual(genes: (0..<10).map { _ in Int.random(in: 0...100) })
}
)
let finalPopulation = solver.solve(maxGenerations: 200)
let bestIndividual = finalPopulation.max { $0.fitness() < $1.fitness() }!
print("Best fitness: \(bestIndividual.fitness())")Using Protocol-Based Operators
var solver = GeneticSolver<MyIndividual>(
populationSize: 50,
crossoverRate: 0.8,
mutationRate: 0.1,
selectionOperator: { MyGeneticOperators.selectionOperator(population: $0) },
crossoverOperator: { MyGeneticOperators.crossoverOperator(parent1: $0, parent2: $1) },
mutationOperator: { MyGeneticOperators.mutationOperator(element: $0) },
replacementOperator: { MyGeneticOperators.replacementOperator(old: $0, new: $1) },
terminationCheck: MyGeneticOperators.fixedGenerationTermination(maxGenerations: 100),
newElement: { MyGeneticOperators.newElement() }
)
let finalPopulation = solver.solve(maxGenerations: 200)
let bestIndividual = finalPopulation.max { $0.fitness() < $1.fitness() }!
print("Best fitness: \(bestIndividual.fitness())")Advanced Usage
Step-by-Step Execution
The solver now supports incremental execution using the step() method:
var solver = GeneticSolver<MyIndividual>(/* ... */)
// Run one generation at a time
while !solver.step() {
print("Generation \(solver.currentGeneration): Best fitness = \(solver.currentPopulation.map { $0.fitness() }.max()!)")
}
// Access current state
print("Final generation: \(solver.currentGeneration)")
print("Population size: \(solver.currentPopulation.count)")Custom Termination Conditions
// Stop when fitness improvement stalls
let adaptiveTermination: TerminationCheck<MyIndividual> = { generation, population in
if generation < 10 { return false }
let currentBest = population.map { $0.fitness() }.max()!
let previousBest = // ... get from history
return abs(currentBest - previousBest) < 0.001
}Elitism Replacement
let elitismReplacement: ReplacementOperator<MyIndividual> = { old, new in
let eliteCount = 2
let sortedOld = old.sorted { $0.fitness() > $1.fitness() }
let elite = Array(sortedOld.prefix(eliteCount))
let sortedNew = new.sorted { $0.fitness() > $1.fitness() }
let rest = Array(sortedNew.prefix(old.count - eliteCount))
return elite + rest
}Roulette Wheel Selection
let rouletteSelection: SelectionOperator<MyIndividual> = { population in
let totalFitness = population.reduce(0) { $0 + $1.fitness() }
func selectOne() -> MyIndividual {
let target = Double.random(in: 0..<totalFitness)
var cumulative = 0.0
for individual in population {
cumulative += individual.fitness()
if cumulative >= target {
return individual
}
}
return population.last!
}
return (selectOne(), selectOne())
}API Reference
Core Types
GeneticElement: Protocol for types that can participate in genetic algorithmsFitnessEvaluatable: Protocol for types that can be evaluated for fitnessGeneticSolver<Element>: Main solver class with state trackingGeneticOperators: Protocol defining core genetic algorithm operations
Operator Types
SelectionOperator<Element>:([Element]) -> (Element, Element)CrossoverOperator<Element>:(Element, Element) -> [Element]MutationOperator<Element>:(Element) -> ElementReplacementOperator<Element>:([Element], [Element]) -> [Element]TerminationCheck<Element>:(Int, [Element]) -> Bool
Default Operators
The GeneticOperators protocol provides default implementations for all genetic algorithm operations:
selectionOperator: Tournament selection with tournament size of 3crossoverOperator: Returns parents unchanged (no crossover)mutationOperator: Returns element unchanged (no mutation)replacementOperator: Generational replacement (replace all)fixedGenerationTermination: Stop after fixed number of generationsnewElement: Must be implemented by conforming types
Examples
Traveling Salesman Problem
struct City {
let x: Double, y: Double
}
struct TSPIndividual: GeneticElement, FitnessEvaluatable {
var route: [Int]
let cities: [City]
func fitness() -> Double {
var totalDistance = 0.0
for i in 0..<route.count {
let current = cities[route[i]]
let next = cities[route[(i + 1) % route.count]]
let distance = sqrt(pow(next.x - current.x, 2) + pow(next.y - current.y, 2))
totalDistance += distance
}
return 1.0 / totalDistance // Higher fitness = shorter distance
}
}Knapsack Problem
struct Item {
let weight: Int
let value: Int
}
struct KnapsackIndividual: GeneticElement, FitnessEvaluatable {
var selection: [Bool]
let items: [Item]
let maxWeight: Int
func fitness() -> Double {
let totalWeight = zip(selection, items).reduce(0) { sum, pair in
sum + (pair.0 ? pair.1.weight : 0)
}
if totalWeight > maxWeight {
return 0.0 // Invalid solution
}
let totalValue = zip(selection, items).reduce(0) { sum, pair in
sum + (pair.0 ? pair.1.value : 0)
}
return Double(totalValue)
}
}Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Code Formatting
This project uses SwiftFormat to maintain consistent code style.
Local Development
- Install SwiftFormat:
``bash brew install swiftformat ``
- Format code locally:
``bash ./scripts/format.sh ``
- Check formatting without making changes:
``bash ./scripts/format.sh --check ``
Pre-commit Hooks
Install pre-commit hooks to automatically format code before commits:
# Install pre-commit
pip install pre-commit
# Install the git hook scripts
pre-commit installCI/CD
- Format Check: Every PR is automatically checked for proper formatting
- Auto-Format: Weekly automated formatting PRs are created if needed
- Pre-commit: Local hooks ensure code is formatted before commits
License
This project is licensed under the MIT License - see the LICENSE file for details.
Package Metadata
Repository: kosikowski/genetic-solver
Default branch: main
README: README.md