Project: Smart Ships Part 2

Success! I’m actually quite surprised that it worked, well not surprised that it worked but surprised that I got it to work with as few errors as I got. It was pretty confusing, but I did manage to logic it out with the help of the book and my head. Some parts of it were ommited in the book and some parts didn’t really make sense, so I had to do it with reference to the previous genetic algorithm that I made.

For example, I did the fitness calculation, the cross-over and mutation in the Ship class instead of creating another class. I also found that the fitness equation which the book gave (1/d)^2 was too small to work with, and had to replace it with (1/d) and alter the way the mating pool was generated. However, some of the code for the gA was remarkably similar to the previous one and could easily be translated, so that helped. I added a text through Pygame that counted the number of generations (or I called it a cycle).

Here is the code (that I’m quite proud of), tidied up with a smattering of comments:

import pygame, sys, numpy, random
from pygame.locals import *

pygame.init()

FPS = 100 # controls how fast each frame goes
fpsClock = pygame.time.Clock()

# set up the window
DISPLAYSURF = pygame.display.set_mode((500, 500))
pygame.display.set_caption('Ship')

# basic setup
WHITE = (255, 255, 255)
shipImg = pygame.image.load('ship.gif')
depot = pygame.image.load('depot.gif')

fontObj = pygame.font.Font('freesansbold.ttf', 15)

class DNA:
    """genotype for the ships"""
    def __init__(self):
        self.genes = [numpy.array([0])]*lifetime # creates a vector for every "frame"
        for i in range(lifetime):
            self.genes[i] = numpy.array([random.randint(-1, 1),
            random.randint(-1,1)])

class Ship:
    """A ship"""
    def __init__(self):
        self.location = numpy.array([0,400]) # initial location at bottom left
        self.velocity = numpy.array([0,0]) # not moving
        self.acceleration = numpy.array([0,0]) # not accelerating
        self.dna = DNA() # each ship has a genotype
        self.ontarget = False 

    def applyForce(self, f):
        self.acceleration += f 

    def update(self):
        self.velocity += self.acceleration # so that velocity adds up
        self.location += self.velocity
        self.acceleration *= 0 # resets the acceleration

    def fitness(self):
        d = numpy.linalg.norm(self.location-numpy.array([247, 96]))
        self.fitness = (1/d) # the smaller the distance, the greater the fitness

    def crossover(self, partner): # creates a child from two parents
        child = Ship()
        midpoint = random.randint(0, len(self.dna.genes))
        for i in range(len(self.dna.genes)):
            if i >= midpoint:
                child.dna.genes[i] = self.dna.genes[i]
            else:
                child.dna.genes[i] = partner.dna.genes[i]
        return child

    def mutate(self): # mutates the genes randomly
        for i in range(len(self.dna.genes)):
            if random.random() < mutationrate:
                self.dna.genes[i] = numpy.array([random.randint(-1, 1),
                random.randint(-1,1)])

lifetime = 50 # how many frames there are, per cycle
lifecounter = 0
cyclecounter = 0

# location of depot
depotx = 200
depoty = 0

population_no = 50 # number of ships
mutationrate = 0.01 # rate of mutation of genes

population = []
for _ in range(population_no): # creates an array of Ships
    population.append(Ship())

def cycle():
    for i in population: # calculates fitness for all population
        i.fitness()

    mating_pool = []
    for i in population:
        n = int(i.fitness * 1000)
        for x in range(n): # adds Ships into mating pool based on fitness
            mating_pool.append(i)

    for i in range(len(population)):
        a = random.randint(0, len(mating_pool)-1)
        b = random.randint(0, len(mating_pool)-1)

        parent_a = mating_pool[a]
        parent_b = mating_pool[b]

        child = parent_a.crossover(parent_b) # creates child based on fitness

        child.mutate()
        population[i] = child # replaces the parent with the child

while True: # the main loop
    DISPLAYSURF.fill(WHITE)
    DISPLAYSURF.blit(depot, (depotx, depoty))
    pygame.draw.rect(DISPLAYSURF, (0,0,0), (depotx, depoty, 96, 96,),1)

    textSurfaceObj = fontObj.render("Cycle: " + str(cyclecounter), True, (0,0,0))
    textRectObj = textSurfaceObj.get_rect()
    textRectObj.bottomright = (500, 500)

    DISPLAYSURF.blit(textSurfaceObj, textRectObj)

    if lifecounter < lifetime:
        for s in population:
            # if any ship is in rectangle, then it's on target and stops moving
            if ((s.location[0] > depotx-32 and s.location[0] < depotx+96-32) and
            (s.location[1] > depoty and s.location[1] < depoty+80)):
                s.ontarget = True

            #if not, then the ships keep accelerating per frame
            if s.ontarget != True:
                s.applyForce(s.dna.genes[lifecounter])
                s.update()
                DISPLAYSURF.blit(shipImg, s.location)
            else:
                DISPLAYSURF.blit(shipImg, s.location)
        lifecounter += 1

    else:
        lifecounter = 0
        cyclecounter += 1
        cycle()

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

    pygame.display.update()
    fpsClock.tick(FPS)

Here are the images of the ship.gif and the depot.gif.

I made a gif (in very low fps and resolution, sorry) of it simulating and in the beginning, the ships are accelerating in random directions with apparently no goal. However by cycle 3-4, the ships are mostly all moving towards the depot, with a few stragglers. By cycle 5-10, the ships are pretty homogenised with no noticeable difference in cycles 10-20, other than random mutations and most reach the depot.

ships