Skip to content

Commit

Permalink
Better async wrapper and small tweaks / additions.
Browse files Browse the repository at this point in the history
  • Loading branch information
sgonzalez committed Aug 8, 2019
1 parent 9409952 commit f0d932d
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 33 deletions.
10 changes: 9 additions & 1 deletion README.md
Expand Up @@ -23,6 +23,10 @@
* **Living Trees** Trees whose structure and node types are evolved.
* **Living Forests** A fixed-size collection of coevolved trees. Evolution of Living Forests is analogous to evolution of a genome of chromosomes.

The library also provides the following primitives that you can use to build your own complex genomes:

* `DiscreteChoiceGene` to represent a discrete choice in a set.


## Usage

Expand All @@ -33,7 +37,11 @@ Everything you need to use **SwiftGenetics** is in the `Sources/` directory. The
3. Set the fitness for each `Organism` in the population's `organisms` array.
4. Repeat steps 2 and 3 *ad infinitum* (or until you're happy with a solution).

However, oftentimes you might have long-running fitness calculations that you want to run concurrently, in which case you can use `EvolutionWrapper` types as the entry point into **SwiftGenetics**. `ConcurrentSynchronousEvaluationGA` and `ConcurrentAsynchronousEvaluationGA` perform synchronous and asynchronous, respectively, fitness evaluations. These wrappers make adding a GA trivial, all you need are an initial population and a type that conforms to `EvolutionLoggingDelegate`.
### Evolution Wrappers

Oftentimes you might have long-running fitness calculations that you want to run concurrently, in which case you can use `EvolutionWrapper` types as the entry point into **SwiftGenetics**. `ConcurrentSynchronousEvaluationGA` and `ConcurrentAsynchronousEvaluationGA` perform synchronous and asynchronous, respectively, fitness evaluations. These wrappers make adding a GA trivial, all you need are an initial population and a type that conforms to `EvolutionLoggingDelegate`.

One cool feature that `ConcurrentAsynchronousEvaluationGA` has is the ability to detect and efficiently handle duplicate genomes within a generation (based on their hash value from a conformance to `Hashable`).

### Concrete Example

Expand Down
10 changes: 9 additions & 1 deletion Sources/Clades/LivingForests/LivingForestGenome.swift
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// An evolvable forest of one or more independent trees.
/// Note: Forests are homogeneous for now.
/// Note: Forests have homogeneous gene types for now.
struct LivingForestGenome<GeneType: TreeGeneType>: Genome {

typealias RealGene = LivingTreeGenome<GeneType>
Expand Down Expand Up @@ -52,3 +52,11 @@ struct LivingForestGenome<GeneType: TreeGeneType>: Genome {
}

}

extension LivingForestGenome: RawRepresentable {
typealias RawValue = [RealGene]
var rawValue: RawValue { return trees }
init?(rawValue: RawValue) {
self = LivingForestGenome.init(trees: rawValue)
}
}
2 changes: 1 addition & 1 deletion Sources/Clades/LivingStrings/LivingStringEnvironment.swift
@@ -1,6 +1,6 @@
//
// LivingStringEnvironment.swift
// lossga
// SwiftGenetics
//
// Created by Santiago Gonzalez on 7/9/19.
// Copyright © 2019 Santiago Gonzalez. All rights reserved.
Expand Down
8 changes: 8 additions & 0 deletions Sources/Clades/LivingStrings/LivingStringGenome.swift
Expand Up @@ -49,3 +49,11 @@ struct LivingStringGenome<GeneType: Gene>: Genome where GeneType.Environment ==
}

}

extension LivingStringGenome: RawRepresentable where RealGene: Hashable {
typealias RawValue = [RealGene]
var rawValue: RawValue { return genes }
init?(rawValue: RawValue) {
self = LivingStringGenome.init(genes: rawValue)
}
}
27 changes: 17 additions & 10 deletions Sources/Clades/LivingTrees/LivingTreeGene.swift
Expand Up @@ -42,22 +42,14 @@ final class LivingTreeGene<GeneType: TreeGeneType>: Gene {

performGeneTypeSpecificMutations(rate: rate, environment: environment)

// Mutate type, same structure.
if geneType.isBinaryType {
geneType = template.binaryTypes.filter { $0 != geneType }.randomElement()!
} else if geneType.isUnaryType {
geneType = template.unaryTypes.filter { $0 != geneType }.randomElement() ?? template.unaryTypes.first!
} else if geneType.isLeafType {
geneType = template.leafTypes.filter { $0 != geneType }.randomElement()!
} else {
fatalError()
}
var madeStructuralMutation = false

// Deletion mutations.
if Double.random(in: 0..<1) < environment.structuralMutationDeletionRate {
if !children.isEmpty {
children = []
geneType = template.leafTypes.randomElement()!
madeStructuralMutation = true
}
}

Expand All @@ -77,6 +69,21 @@ final class LivingTreeGene<GeneType: TreeGeneType>: Gene {
} else {
fatalError()
}
madeStructuralMutation = true
}
}

// Attempt to mutate type, maintaining the same structure, only if a
// structural mutation has not already been made.
if !madeStructuralMutation {
if geneType.isBinaryType {
geneType = template.binaryTypes.filter { $0 != geneType }.randomElement()!
} else if geneType.isUnaryType {
geneType = template.unaryTypes.filter { $0 != geneType }.randomElement() ?? template.unaryTypes.first!
} else if geneType.isLeafType {
geneType = template.leafTypes.filter { $0 != geneType }.randomElement()!
} else {
fatalError()
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Clades/LivingTrees/LivingTreeGenome.swift
Expand Up @@ -63,6 +63,14 @@ struct LivingTreeGenome<GeneType: TreeGeneType>: Genome {

}

extension LivingTreeGenome: RawRepresentable {
typealias RawValue = RealGene
var rawValue: RawValue { return rootGene }
init?(rawValue: RawValue) {
self = LivingTreeGenome.init(rootGene: rawValue)
}
}

// Living trees can behave as genes within a living forest genome.
extension LivingTreeGenome: Gene {
typealias Environment = RealGene.Environment
Expand Down
11 changes: 10 additions & 1 deletion Sources/Clades/LivingTrees/TreeGeneType.swift
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// An abstract interface that all tree gene types conform to.
protocol TreeGeneType: Equatable {
protocol TreeGeneType: Hashable {
var childCount: Int { get }
var isBinaryType: Bool { get }
var isUnaryType: Bool { get }
Expand All @@ -30,6 +30,7 @@ extension TreeGeneType {
static var allTypes: [Self] { return nonLeafTypes + leafTypes }
}


/// Templates can enforce certain constraints and define gene type sampling.
struct TreeGeneTemplate<T: TreeGeneType> {
/// Sampling array for binary gene types.
Expand All @@ -44,3 +45,11 @@ struct TreeGeneTemplate<T: TreeGeneType> {
/// A sampling array for all types.
var allTypes: [T] { return nonLeafTypes + leafTypes }
}

extension TreeGeneTemplate: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(binaryTypes)
hasher.combine(unaryTypes)
hasher.combine(leafTypes)
}
}
33 changes: 33 additions & 0 deletions Sources/Clades/Primitive Genes/DiscreteChoiceGene.swift
@@ -0,0 +1,33 @@
//
// DiscreteChoiceGene.swift
// SwiftGenetics
//
// Created by Santiago Gonzalez on 7/25/19.
// Copyright © 2019 Santiago Gonzalez. All rights reserved.
//

import Foundation

protocol DiscreteChoice: CaseIterable, Hashable { }

struct DiscreteChoiceGene<C: DiscreteChoice, E: GeneticEnvironment>: Gene {
typealias Environment = E

var choice: C

mutating func mutate(rate: Double, environment: DiscreteChoiceGene<C, E>.Environment) {
guard Double.random(in: 0..<1) < rate else { return }

// Select a new choice randomly.
choice = C.allCases.filter { $0 != choice }.randomElement()!
}
}

extension DiscreteChoiceGene: RawRepresentable {
typealias RawValue = C
var rawValue: RawValue { return choice }
init?(rawValue: RawValue) {
self = DiscreteChoiceGene.init(choice: rawValue)
}
}

Expand Up @@ -12,7 +12,7 @@ import Foundation

/// Encapsulates a generic genetic algorithm that performs asynchronous fitness
/// evaluations concurrently. The fitness evaluator needs to be thread-safe.
final class ConcurrentAsynchronousEvaluationGA<Eval: AsynchronousFitnessEvaluator, LogDelegate: EvolutionLoggingDelegate> : EvolutionWrapper where Eval.G == LogDelegate.G {
final class ConcurrentAsynchronousEvaluationGA<Eval: AsynchronousFitnessEvaluator, LogDelegate: EvolutionLoggingDelegate> : EvolutionWrapper where Eval.G == LogDelegate.G, Eval.G: Hashable {

var fitnessEvaluator: Eval

Expand All @@ -29,14 +29,31 @@ final class ConcurrentAsynchronousEvaluationGA<Eval: AsynchronousFitnessEvaluato
for i in 0..<maxEpochs {
// Log start of epoch.
loggingDelegate.evolutionStartingEpoch(i)
let startDate = Date()

// Perform an epoch.
population.epoch()

// Schedule fitness evals.

var remainingRequests = population.organisms.count
let remainingRequestsSem = DispatchSemaphore(value: 1)
let completionSem = DispatchSemaphore(value: 0)

/// Updates the remaining request and loop completion variables in a
/// thread-safe manner.
func decrementRequests() {
remainingRequestsSem.wait()
remainingRequests -= 1
if remainingRequests == 0 {
completionSem.signal()
}
remainingRequestsSem.signal()
}

var evalDependencies = [Eval.G: [Organism<Eval.G>]]()

// Iterate over the population.
for organism in population.organisms {
guard organism.fitness == nil else {
remainingRequestsSem.wait()
Expand All @@ -45,41 +62,62 @@ final class ConcurrentAsynchronousEvaluationGA<Eval: AsynchronousFitnessEvaluato
continue
}

// Check if we've already requested the fitness for this genotype.
let alreadyRequested = evalDependencies[organism.genotype] != nil
if alreadyRequested {
evalDependencies[organism.genotype]!.append(organism)
} else {
// Make a note of the request.
evalDependencies[organism.genotype] = []
}

DispatchQueue.global().async {
self.fitnessEvaluator.requestFitnessFor(organism: organism)

remainingRequestsSem.wait()
remainingRequests -= 1
if remainingRequests == 0 {
completionSem.signal()
if !alreadyRequested { // Avoid duplicate requests.
self.fitnessEvaluator.requestFitnessFor(organism: organism)
}
remainingRequestsSem.signal()
decrementRequests()
}
}
completionSem.wait()

// Retrieve fitness evals.
var evalDependencyResults = [Eval.G: Double?]()
let evalDependencyResultsSem = DispatchSemaphore(value: 1)
while population.organisms.contains(where: { $0.fitness == nil }) {
let remainingOrganisms = population.organisms.filter({ $0.fitness == nil })
remainingRequests = remainingOrganisms.count
let organismsSem = DispatchSemaphore(value: 1)
for organism in remainingOrganisms {
// Check if our dependency has results yet.
evalDependencyResultsSem.wait()
let potentialResult = evalDependencyResults[organism.genotype]
evalDependencyResultsSem.signal()
if let result = potentialResult {
organism.fitness = result
decrementRequests()
continue
}

DispatchQueue.global().async {
organism.fitness = self.fitnessEvaluator.fitnessResultFor(organism: organism)

remainingRequestsSem.wait()
remainingRequests -= 1
if remainingRequests == 0 {
completionSem.signal()
if let result = self.fitnessEvaluator.fitnessResultFor(organism: organism) {
organismsSem.wait()
organism.fitness = result
organismsSem.signal()
evalDependencyResultsSem.wait()
evalDependencyResults[organism.genotype] = result
evalDependencyResultsSem.signal()
}
remainingRequestsSem.signal()

decrementRequests()
}
}
completionSem.wait()
sleep(1) // Arbitrary sleep to avoid making things go crazy if the fitness evaluation check is very fast.
}

// Print epoch statistics.
loggingDelegate.evolutionFinishedEpoch(i, population: population)
let elapsedInterval = Date().timeIntervalSince(startDate)
loggingDelegate.evolutionFinishedEpoch(i, duration: elapsedInterval, population: population)
}

}
Expand Down
Expand Up @@ -29,6 +29,7 @@ final class ConcurrentSynchronousEvaluationGA<Eval: SynchronousFitnessEvaluator,
for i in 0..<maxEpochs {
// Log start of epoch.
loggingDelegate.evolutionStartingEpoch(i)
let startDate = Date()

// Perform an epoch.
population.epoch()
Expand Down Expand Up @@ -61,7 +62,8 @@ final class ConcurrentSynchronousEvaluationGA<Eval: SynchronousFitnessEvaluator,
completionSem.wait()

// Print epoch statistics.
loggingDelegate.evolutionFinishedEpoch(i, population: population)
let elapsedInterval = Date().timeIntervalSince(startDate)
loggingDelegate.evolutionFinishedEpoch(i, duration: elapsedInterval, population: population)
}

}
Expand Down
11 changes: 10 additions & 1 deletion Sources/Evolution/Abstractions/EvolutionLoggingDelegate.swift
Expand Up @@ -13,9 +13,18 @@ protocol EvolutionLoggingDelegate {
associatedtype G: Genome

/// Called at the beginning of an epoch.
/// - Parameter i: The epoch index.
func evolutionStartingEpoch(_ i: Int)

/// Called at the end of an epoch.
func evolutionFinishedEpoch(_ i: Int, population: Population<G>)
/// - Parameter i: The epoch index.
/// - Parameter duration: How long the epoch took to run (elapsed wall-clock time).
/// - Parameter population: The population at the end of the epoch.
func evolutionFinishedEpoch(_ i: Int, duration: TimeInterval, population: Population<G>)

/// Called when the stopping condition has been met.
/// - Parameter solution: The genome that met the stopping condition.
/// - Parameter fitness: The solution genome's fitness.
func evolutionFoundSolution(_ solution: G, fitness: Double)

}
9 changes: 8 additions & 1 deletion Sources/Genetics/GeneticStructures.swift
Expand Up @@ -25,7 +25,7 @@ protocol Gene: Mutatable {

/// A collection of genes.
protocol Genome: Mutatable, Crossoverable {
associatedtype RealGene: Gene

}

/// Represents a specific, individual organism with a fitness and genome.
Expand Down Expand Up @@ -59,3 +59,10 @@ extension Organism: Comparable {
return lhs.fitness == rhs.fitness
}
}

// Organisms are hashable by their UUID.
extension Organism: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
}

0 comments on commit f0d932d

Please sign in to comment.