diff --git a/README.md b/README.md index e8a4c1f..c49f45d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/Sources/Clades/LivingForests/LivingForestGenome.swift b/Sources/Clades/LivingForests/LivingForestGenome.swift index 1f3727a..9804434 100644 --- a/Sources/Clades/LivingForests/LivingForestGenome.swift +++ b/Sources/Clades/LivingForests/LivingForestGenome.swift @@ -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: Genome { typealias RealGene = LivingTreeGenome @@ -52,3 +52,11 @@ struct LivingForestGenome: Genome { } } + +extension LivingForestGenome: RawRepresentable { + typealias RawValue = [RealGene] + var rawValue: RawValue { return trees } + init?(rawValue: RawValue) { + self = LivingForestGenome.init(trees: rawValue) + } +} diff --git a/Sources/Clades/LivingStrings/LivingStringEnvironment.swift b/Sources/Clades/LivingStrings/LivingStringEnvironment.swift index 2798874..c5d00ea 100644 --- a/Sources/Clades/LivingStrings/LivingStringEnvironment.swift +++ b/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. diff --git a/Sources/Clades/LivingStrings/LivingStringGenome.swift b/Sources/Clades/LivingStrings/LivingStringGenome.swift index 7301e1c..04961b9 100644 --- a/Sources/Clades/LivingStrings/LivingStringGenome.swift +++ b/Sources/Clades/LivingStrings/LivingStringGenome.swift @@ -49,3 +49,11 @@ struct LivingStringGenome: 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) + } +} diff --git a/Sources/Clades/LivingTrees/LivingTreeGene.swift b/Sources/Clades/LivingTrees/LivingTreeGene.swift index d205a33..88799bf 100644 --- a/Sources/Clades/LivingTrees/LivingTreeGene.swift +++ b/Sources/Clades/LivingTrees/LivingTreeGene.swift @@ -42,22 +42,14 @@ final class LivingTreeGene: 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 } } @@ -77,6 +69,21 @@ final class LivingTreeGene: 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() } } } diff --git a/Sources/Clades/LivingTrees/LivingTreeGenome.swift b/Sources/Clades/LivingTrees/LivingTreeGenome.swift index a41c6ee..1d6a438 100644 --- a/Sources/Clades/LivingTrees/LivingTreeGenome.swift +++ b/Sources/Clades/LivingTrees/LivingTreeGenome.swift @@ -63,6 +63,14 @@ struct LivingTreeGenome: 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 diff --git a/Sources/Clades/LivingTrees/TreeGeneType.swift b/Sources/Clades/LivingTrees/TreeGeneType.swift index fb8df90..33438a4 100644 --- a/Sources/Clades/LivingTrees/TreeGeneType.swift +++ b/Sources/Clades/LivingTrees/TreeGeneType.swift @@ -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 } @@ -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 { /// Sampling array for binary gene types. @@ -44,3 +45,11 @@ struct TreeGeneTemplate { /// 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) + } +} diff --git a/Sources/Clades/Primitive Genes/DiscreteChoiceGene.swift b/Sources/Clades/Primitive Genes/DiscreteChoiceGene.swift new file mode 100644 index 0000000..c9a6ddd --- /dev/null +++ b/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: Gene { + typealias Environment = E + + var choice: C + + mutating func mutate(rate: Double, environment: DiscreteChoiceGene.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) + } +} + diff --git a/Sources/Evolution/Abstractions/ConcurrentAsynchronousEvaluationGA.swift b/Sources/Evolution/Abstractions/ConcurrentAsynchronousEvaluationGA.swift index c08f171..5c3260a 100644 --- a/Sources/Evolution/Abstractions/ConcurrentAsynchronousEvaluationGA.swift +++ b/Sources/Evolution/Abstractions/ConcurrentAsynchronousEvaluationGA.swift @@ -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 : EvolutionWrapper where Eval.G == LogDelegate.G { +final class ConcurrentAsynchronousEvaluationGA : EvolutionWrapper where Eval.G == LogDelegate.G, Eval.G: Hashable { var fitnessEvaluator: Eval @@ -29,14 +29,31 @@ final class ConcurrentAsynchronousEvaluationGA]]() + + // Iterate over the population. for organism in population.organisms { guard organism.fitness == nil else { remainingRequestsSem.wait() @@ -45,33 +62,53 @@ final class ConcurrentAsynchronousEvaluationGA) + /// - 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) + /// 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) + } diff --git a/Sources/Genetics/GeneticStructures.swift b/Sources/Genetics/GeneticStructures.swift index 0d846d7..15f7a08 100644 --- a/Sources/Genetics/GeneticStructures.swift +++ b/Sources/Genetics/GeneticStructures.swift @@ -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. @@ -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) + } +}