Skip to content

Commit

Permalink
Merge pull request #3 from mamaral/crossover
Browse files Browse the repository at this point in the history
Crossover
  • Loading branch information
Mike Amaral committed Apr 25, 2015
2 parents f1159af + 053a602 commit eb84b55
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 45 deletions.
4 changes: 2 additions & 2 deletions Evolve.podspec
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Pod::Spec.new do |s|

s.name = "Evolve"
s.version = "0.3"
s.version = "0.4"
s.summary = "An Objective-C evolution simulation engine."
s.homepage = "https://github.com/mamaral/Evolve"
s.license = "MIT"
s.author = { "Mike Amaral" => "[email protected]" }
s.social_media_url = "http://twitter.com/MikeAmaral"
s.platform = :ios
s.source = { :git => "https://github.com/mamaral/Evolve.git", :tag => "v0.3" }
s.source = { :git => "https://github.com/mamaral/Evolve.git", :tag => "v0.4" }
s.source_files = "Source/EvolutionManager.{h,m}", "Source/Population.{h,m}", "Source/Organism.{h,m}", "Source/Genome.{h,m}", "Source/Random.{h,m}"
s.requires_arc = true

Expand Down
2 changes: 1 addition & 1 deletion EvolveTests/EvolutionManagerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ - (void)tearDown {
- (void)testInitWithPopulation {
for (NSInteger i = 0; i < kEvolutionManagerTestIterations; i++) {
NSInteger randomSize = [Random randomIntegerFromMin:2 toMax:100];
NSInteger randomLength = [Random randomIntegerFromMin:1 toMax:10];
NSInteger randomLength = [Random randomIntegerFromMin:4 toMax:10];
NSString *domain = @"abcd";
Population *population = [[Population alloc] initRandomPopulationWithSize:randomSize geneSequenceLength:randomLength genomeDomain:domain];
EvolutionManager *manager = [[EvolutionManager alloc] initWithPopulation:population];
Expand Down
27 changes: 21 additions & 6 deletions EvolveTests/GenomeTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ - (void)testInitWithGeneSequenceAndDomain {
NSString *entireDomain = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789";

for (NSInteger i = 0; i < kGenomeTestIterations; i++) {
NSInteger randomSequenceLength = [Random randomIntegerFromMin:1 toMax:50];
NSInteger randomSequenceLength = [Random randomIntegerFromMin:4 toMax:50];
NSInteger randomDomainLength = [Random randomIntegerFromMin:1 toMax:25];
NSString *randomDomain = [Random randomGeneSequenceWithLength:randomDomainLength domain:entireDomain];
NSString *randomGeneSequence = [Random randomGeneSequenceWithLength:randomSequenceLength domain:randomDomain];
Expand All @@ -40,7 +40,7 @@ - (void)testInitRandomWithLengthAndDomain {
NSString *entireDomain = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789";

for (NSInteger i = 0; i < kGenomeTestIterations; i++) {
NSInteger randomSequenceLength = [Random randomIntegerFromMin:1 toMax:50];
NSInteger randomSequenceLength = [Random randomIntegerFromMin:4 toMax:50];
NSInteger randomDomainLength = [Random randomIntegerFromMin:1 toMax:25];
NSString *randomDomain = [Random randomGeneSequenceWithLength:randomDomainLength domain:entireDomain];

Expand Down Expand Up @@ -108,11 +108,10 @@ - (void)testInitRandomWithInvalidDomain {
XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
}

- (void)testHandleMutation {
// With a 100% mutation rate (the only thing we can really effectively test)
// a gene should never be the same after mutation.
- (void)testHandleMutationGuarenteed {
// With a 100% mutation rate a gene should never be the same after mutation.
NSString *testDomain = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Genome *testGenome = [[Genome alloc] initRandomGenomeWithLength:1 domain:testDomain];
Genome *testGenome = [[Genome alloc] initRandomGenomeWithLength:4 domain:testDomain];

for (NSInteger i = 0; i < kGenomeTestIterations; i++) {
NSString *originalGeneSequence = testGenome.sequence;
Expand All @@ -125,4 +124,20 @@ - (void)testHandleMutation {
}
}

- (void)testHandleMutationShouldNotHappen {
// With a 0% mutation rate a gene should be the same after mutation.
NSString *testDomain = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Genome *testGenome = [[Genome alloc] initRandomGenomeWithLength:4 domain:testDomain];

for (NSInteger i = 0; i < kGenomeTestIterations; i++) {
NSString *originalGeneSequence = testGenome.sequence;

[testGenome handleMutationWithRate:0.0];

NSString *newGeneSequence = testGenome.sequence;

XCTAssertTrue([newGeneSequence isEqualToString:originalGeneSequence]);
}
}

@end
58 changes: 43 additions & 15 deletions EvolveTests/OrganismTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

static NSInteger const kOrganismTestIterations = 10000;

@interface Organism (Testing)

+ (NSRange)generateRangeForTwoPointCrossoverFromLength:(NSUInteger)geneSequenceLength;

@end

@interface OrganismTests : XCTestCase

@end
Expand All @@ -36,7 +42,7 @@ - (void)testInitRandom {
NSString *testDomain = @"abcdefg 123456";

for (NSInteger i = 0; i < kOrganismTestIterations; i++) {
NSInteger randomLength = [Random randomIntegerFromMin:1 toMax:25];
NSInteger randomLength = [Random randomIntegerFromMin:4 toMax:25];
Organism *organism = [[Organism alloc] initRandomWithGeneSequenceLength:randomLength domain:testDomain];

XCTAssertNotNil(organism);
Expand Down Expand Up @@ -97,7 +103,7 @@ - (void)testGenerateOffspring {
for (NSInteger i = 0; i < kOrganismTestIterations; i++) {
Organism *parent1 = [[Organism alloc] initRandomWithGeneSequenceLength:testGeneSequenceLength domain:testDomain];
Organism *parent2 = [[Organism alloc] initRandomWithGeneSequenceLength:testGeneSequenceLength domain:testDomain];
Organism *offspring = [Organism offspringFromParent1:parent1 parent2:parent2 mutationRate:0.0];
Organism *offspring = [parent1 mateWithOrganism:parent2 crossoverMethod:CrossoverMethodOnePoint mutationRate:0.0];

XCTAssertNotNil(offspring);
XCTAssertNotNil(offspring.genome);
Expand All @@ -122,17 +128,9 @@ - (void)testGenerateOffspring {
}
}

- (void)testGenerateOffspringWithInvalidParent1 {
- (void)testGenerateOffspringWithInvalidMate {
void (^expressionBlock)() = ^{
__unused Organism *offspring = [Organism offspringFromParent1:nil parent2:[Organism new] mutationRate:0.0];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
}

- (void)testGenerateOffspringWithInvalidParent2 {
void (^expressionBlock)() = ^{
__unused Organism *offspring = [Organism offspringFromParent1:[Organism new] parent2:nil mutationRate:0.0];
__unused Organism *offspring = [[Organism new] mateWithOrganism:nil crossoverMethod:CrossoverMethodOnePoint mutationRate:0.0];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
Expand All @@ -144,7 +142,7 @@ - (void)testGenerateOffspringWithInvalidMutationRate {
NSInteger testLength = 4;
Organism *parent1 = [[Organism alloc] initRandomWithGeneSequenceLength:testLength domain:testDomain];
Organism *parent2 = [[Organism alloc] initRandomWithGeneSequenceLength:testLength domain:testDomain];
__unused Organism *offspring = [Organism offspringFromParent1:parent1 parent2:parent2 mutationRate:1.5];
__unused Organism *offspring = [parent1 mateWithOrganism:parent2 crossoverMethod:CrossoverMethodOnePoint mutationRate:1.5];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
Expand All @@ -155,7 +153,7 @@ - (void)testGenerateOffspringWithMismatchingParentLength {
NSString *testDomain = @"abcd";
Organism *parent1 = [[Organism alloc] initRandomWithGeneSequenceLength:3 domain:testDomain];
Organism *parent2 = [[Organism alloc] initRandomWithGeneSequenceLength:4 domain:testDomain];
__unused Organism *offspring = [Organism offspringFromParent1:parent1 parent2:parent2 mutationRate:0.0];
__unused Organism *offspring = [parent1 mateWithOrganism:parent2 crossoverMethod:CrossoverMethodOnePoint mutationRate:0.0];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
Expand All @@ -166,7 +164,37 @@ - (void)testGenerateOffspringWithMismatchingParentDomain {
NSInteger testLength = 4;
Organism *parent1 = [[Organism alloc] initRandomWithGeneSequenceLength:testLength domain:@"abc"];
Organism *parent2 = [[Organism alloc] initRandomWithGeneSequenceLength:testLength domain:@"123"];
__unused Organism *offspring = [Organism offspringFromParent1:parent1 parent2:parent2 mutationRate:0.0];
__unused Organism *offspring = [parent1 mateWithOrganism:parent2 crossoverMethod:CrossoverMethodOnePoint mutationRate:0.0];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
}

- (void)testGenerateRangeForTwoPointCrossoverWithMinimumLength {
for (NSInteger i = 0; i < kOrganismTestIterations; i++) {
NSRange range = [Organism generateRangeForTwoPointCrossoverFromLength:kMinimumGeneSequenceLength];

XCTAssertEqual(range.location, 1);
XCTAssertEqual(range.length, 1);
}
}

- (void)testGenerateRangeForTwoPointCrossoverWithRandomLength {
for (NSInteger i = 0; i < kOrganismTestIterations; i++) {
NSUInteger randomLength = [Random randomIntegerFromMin:4 toMax:100];
NSRange range = [Organism generateRangeForTwoPointCrossoverFromLength:randomLength];
NSUInteger lastIndexFromRange = range.location + range.length + 1;

XCTAssertGreaterThanOrEqual(range.location, 1);
XCTAssertGreaterThanOrEqual(range.length, 1);
XCTAssertLessThan(lastIndexFromRange, randomLength);
}
}

- (void)testGenerateRangeForTwoPointCrossoverWithInvalidLength {
void (^expressionBlock)() = ^{
NSInteger randomInvalidLength = [Random randomIntegerFromMin:0 toMax:kMinimumGeneSequenceLength - 1];
[Organism generateRangeForTwoPointCrossoverFromLength:randomInvalidLength];
};

XCTAssertThrowsSpecificNamed(expressionBlock(), NSException, NSInternalInconsistencyException);
Expand Down
4 changes: 2 additions & 2 deletions EvolveTests/PopulationTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ - (void)testInitWithOrganisms {
NSMutableArray *organisms = [NSMutableArray arrayWithCapacity:numberOfOrganisms];

for (NSInteger i = 0; i < numberOfOrganisms; i++) {
[organisms addObject:[[Organism alloc] initRandomWithGeneSequenceLength:1 domain:@"abcd"]];
[organisms addObject:[[Organism alloc] initRandomWithGeneSequenceLength:4 domain:@"abcd"]];
}

Population *population = [[Population alloc] initWithOrganisms:organisms];
Expand All @@ -40,7 +40,7 @@ - (void)testInitWithOrganisms {
- (void)testInitRandom {
for (NSInteger i = 0; i < kPopulationTestIterations; i++) {
NSInteger randomSize = [Random randomIntegerFromMin:2 toMax:20];
NSInteger randomGeneSequenceLength = [Random randomIntegerFromMin:2 toMax:30];
NSInteger randomGeneSequenceLength = [Random randomIntegerFromMin:4 toMax:30];
NSString *domain = @"abcdefg1234567";

Population *population = [[Population alloc] initRandomPopulationWithSize:randomSize geneSequenceLength:randomGeneSequenceLength genomeDomain:domain];
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Evolve is a customizable [***evolutionary algorithm***](http://en.wikipedia.org/
- [x] **Customizable genomes**. You may want your genome to represent a color value, so you choose to define the domain of the genome to be all hex characters `0123456789ABCDEF` and the length to be `6`, so you can easily translate a gene sequence like `FF0000` to the color red. You might want your domain to be a binary string, `01` with the length `6`, like `110100`, where the first two digits represent the speed of your organism, the next two represent the strength of your organism, and the last two represent how well they're camouflaged. You define what the genome is and how it manifests itself in the wild, the possibilities are endless. Be creative!
- [x] **Constant population size** from generation to generation.
- [x] **Tournament style selection.**
- [x] **Single-point crossover.**
- [x] **One-point or Two-point crossover.**
- [x] **Uniform mutation.**
- [x] ***Tweakable*** **simulation parameters.** Play around with some of the settable variables to see how they effect the simulation. *Tweak the dials*, make predictions and test your hypotheses in real-time!
- [x] **Flexible simulation mechanics.** The engine doesn't continue with the next generation whenever it wants, it waits for you to tell it to continue. Because of this, you can run the simulation in a for-loop running through thousands of generations almost instantly, or you can take each organism in your population and put them in a real-time interactive simulation using something like SpriteKit, making each generation take minutes, or even hours, if that's what interests you.
Expand Down Expand Up @@ -123,9 +123,11 @@ Separate from the parent selection process, the population is sorted based on ea
# *Crossover and Mutation*

This algorithm simulates sexual reproduction, as opposed to asexual reproduction, which means that offspring are always generated from two distinct parents, where the resulting child has the same genetic characteristics as both parents, meaning the `length` and `domain` of the genome remain the same. When the two parents are selected from the pool, their genes are combined using the [***One-point crossover***](http://en.wikipedia.org/wiki/Crossover_(genetic_algorithm)#One-point_crossover) technique. A random index is chosen between `0` and `length - 1` of the gene sequence, both genomes are copied, "split" at this index, and recombined to form the child's genetic code.
This algorithm simulates sexual reproduction, as opposed to asexual reproduction, which means that offspring are always generated from two distinct parents, where the resulting child has the same genetic characteristics as both parents, meaning the `length` and `domain` of the genome remain the same. When the two parents are selected from the pool, their genes are combined using either the [***One-point crossover***](http://en.wikipedia.org/wiki/Crossover_(genetic_algorithm)#One-point_crossover) or [***Two-point crossover***](http://en.wikipedia.org/wiki/Crossover_%28genetic_algorithm%29#Two-point_crossover) techniques. Single-point crossover results in a random index being chosen between `1` and `length - 2` of the gene sequence, both genomes are copied, "split" at this index, and recombined to form the child's genetic code. The range of this index ensures that at least one gene from each parent makes it into the offspring's initial genome. Two-point crossover results in a random range being selected with the same restrictions as above, and that range is used to generate a "slice" from the genome where one parent contributes their genes from before and after the range, and the other contributes their genes from the range itself.

>***Example:*** If the domain is defined as `@"AB"`, with the first parent's gene sequence as `AAAAA` and the second's as `BBBBB`, if the randomly chosen index was `2`, the first parent's contribution would be `AA`, and the second parent's contribution would be `BBB`, resulting in the child's genetic sequence of `AABBB`.
>***One-point Example:*** Consider a domain defined as `@"AB"`, with the first parent's gene sequence as `AAAAA` and the second's as `BBBBB`. If the randomly chosen index was `2`, the first parent's contribution would be `AA`, and the second parent's contribution would be `BBB`, resulting in the child's genetic sequence being `AABBB`.
>***Two-point example*** Consider a domain defined as `@"ABCD1234"`, with the first parent's gene sequence as `AABBCCDD` and the second's as `11223344`. If the randomly generate range was `(2,3)`, that range would be used to select `223` from the second parent's genes, as well as `AA` and `CDD` from the first parent's genes, resulting in the child's genetic sequence being `AA223CDD`.
After the crossover process is complete, every gene in the offspring's genetic sequence has a chance to [***mutate***](http://en.wikipedia.org/wiki/Mutation_(genetic_algorithm)). This algorithm utilizes a uniform mutation strategy, meaning that if by chance the gene does mutate, it will mutate into one of the characters defined by the domain of their genome, ***not including the current character***, with an equal chance to mutate into each.

Expand Down
1 change: 1 addition & 0 deletions Source/EvolutionManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

@property (nonatomic) NSUInteger currentGeneration;

@property (nonatomic) enum CrossoverMethod crossoverMethod;
@property (nonatomic) NSUInteger tournamentSize;
@property (nonatomic) CGFloat elitismPercentage;
@property (nonatomic) CGFloat mutationRate;
Expand Down
4 changes: 3 additions & 1 deletion Source/EvolutionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import "EvolutionManager.h"

static enum CrossoverMethod const kDefaultCrossoverMethod = CrossoverMethodOnePoint;
static CGFloat const kDefaultMutationRate = 0.05;
static CGFloat const kDefaultElitismPercentage = 0.10;
static NSInteger const kDefaultTournamentSize = 2;
Expand All @@ -30,6 +31,7 @@ - (instancetype)initWithPopulation:(Population *)population {
self.population = population;

// Set our defaults.
self.crossoverMethod = kDefaultCrossoverMethod;
self.mutationRate = kDefaultMutationRate;
self.elitismPercentage = kDefaultElitismPercentage;
self.tournamentSize = self.population.organisms.count > 3 ? kDefaultTournamentSize : 1;
Expand Down Expand Up @@ -130,7 +132,7 @@ - (NSArray *)generateOffspringFromOrganisms:(NSArray *)parents count:(NSUInteger
continue;
}

Organism *child = [Organism offspringFromParent1:parent1 parent2:parent2 mutationRate:self.mutationRate];
Organism *child = [parent1 mateWithOrganism:parent2 crossoverMethod:self.crossoverMethod mutationRate:self.mutationRate];

// Add this child to our array of offspring.
[offspring addObject:child];
Expand Down
2 changes: 2 additions & 0 deletions Source/Genome.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#import <UIKit/UIKit.h>

static NSUInteger const kMinimumGeneSequenceLength = 4;

@interface Genome : NSObject

@property (nonatomic, strong) NSString *domain;
Expand Down
3 changes: 2 additions & 1 deletion Source/Genome.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "Genome.h"
#import "Random.h"


@implementation Genome

#pragma mark - Initializers
Expand All @@ -25,7 +26,7 @@ - (instancetype)initWithGeneSequence:(NSString *)geneSequence domain:(NSString *
}

NSParameterAssert(geneSequence);
NSParameterAssert(geneSequence.length > 0);
NSParameterAssert(geneSequence.length >= kMinimumGeneSequenceLength);
NSParameterAssert(domain);
NSParameterAssert(domain.length > 0);

Expand Down
7 changes: 6 additions & 1 deletion Source/Organism.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
#import <Foundation/Foundation.h>
#import "Genome.h"

typedef NS_ENUM(NSUInteger, CrossoverMethod) {
CrossoverMethodOnePoint,
CrossoverMethodTwoPoint
};

@interface Organism : NSObject

@property (nonatomic, strong) Genome *genome;
Expand All @@ -18,6 +23,6 @@
- (instancetype)initWithGenome:(Genome *)genome;
- (instancetype)initRandomWithGeneSequenceLength:(NSUInteger)length domain:(NSString *)domain;

+ (instancetype)offspringFromParent1:(Organism *)parent1 parent2:(Organism *)parent2 mutationRate:(CGFloat)mutationRate;
- (instancetype)mateWithOrganism:(Organism *)mate crossoverMethod:(enum CrossoverMethod)crossoverMethod mutationRate:(CGFloat)mutationRate;

@end
Loading

0 comments on commit eb84b55

Please sign in to comment.