Home Reference Source

src/js/molecules/geneticAlgorithm.js

import Atom from '../prototypes/atom.js'
import GlobalVariables from '../globalvariables'

/**
 * This class creates the Genetic Algorithm atom.
 */
export default class GeneticAlgorithm extends Atom {
    
    /**
     * The constructor function.
     * @param {object} values An array of values passed in which will be assigned to the class as this.x
     */ 
    constructor(values){
        
        super(values)
        
        /**
         * This atom's name
         * @type {string}
         */
        this.name = 'Genetic Algorithm'
        
        /**
         * This atom's type
         * @type {string}
         */
        this.atomType = 'Genetic Algorithm'
        /** 
         * A description of this atom
         * @type {string}
         */
        this.description = "Defines a new genetic algorithm which will simulate evolution to maximize the fitness function using the input population size and number of generations. Only constants which are upstream of this atom with evolve checked will be changed."
        
        /**
         * An array of constant objects which need to be evolved
         * @type {array}
         */
        this.constantsToEvolve = []
        
        /**
         * An array of objects representing the current population
         * @type {array}
         */
        this.population = []
        
        /**
         * Current individual being evaluated in this generation
         * @type {integer}
         */
        this.individualIndex = 0
         
        /**
         * Current generation
         * @type {integer}
         */
        this.generation = 0
        
        /**
         * Top fitness value for the current generation
         * @type {float}
         */
        this.topFitness = 0
        
        /**
         * A flag to indicate if evolution is in process
         * @type {boolean}
         */
        this.evolutionInProcess = false
        
        this.addIO('input', 'fitness function', this, 'number', 0)
        this.addIO('input', 'population size', this, 'number', 50)
        this.addIO('input', 'number of generations', this, 'number', 10)
        
        this.setValues(values)
        
        this.updateValue()
    }

    /**
     * Draw the code atom which has a code icon.
     */ 
    draw(){

        super.draw() //Super call to draw the rest
         

        const xInPixels = GlobalVariables.widthToPixels(this.x)
        const yInPixels = GlobalVariables.heightToPixels(this.y)
        const radiusInPixels = GlobalVariables.widthToPixels(this.radius)
      
       
        GlobalVariables.c.beginPath()
        GlobalVariables.c.arc(GlobalVariables.widthToPixels(this.x - this.radius/5), 
            GlobalVariables.heightToPixels(this.y), 
            GlobalVariables.widthToPixels(this.radius/2), Math.PI *3.4, Math.PI * 2.7, false) 
        GlobalVariables.c.stroke() 
        GlobalVariables.c.closePath()

        GlobalVariables.c.beginPath()
        GlobalVariables.c.arc(GlobalVariables.widthToPixels(this.x + this.radius/5), 
            GlobalVariables.heightToPixels(this.y ), 
            GlobalVariables.widthToPixels(this.radius/2), Math.PI *3.6, Math.PI * 2.3, true) 
        GlobalVariables.c.stroke() 
        GlobalVariables.c.closePath()  

        GlobalVariables.c.beginPath()
        GlobalVariables.c.fillStyle = '#949294'
        GlobalVariables.c.moveTo(xInPixels - radiusInPixels/3, yInPixels )
        GlobalVariables.c.lineTo(xInPixels + radiusInPixels/3, yInPixels )
        GlobalVariables.c.stroke()
        GlobalVariables.c.closePath()

        GlobalVariables.c.beginPath()
        GlobalVariables.c.fillStyle = '#949294'
        GlobalVariables.c.moveTo(xInPixels - radiusInPixels/3, yInPixels - radiusInPixels/5 )
        GlobalVariables.c.lineTo(xInPixels + radiusInPixels/3, yInPixels - radiusInPixels/5)
        GlobalVariables.c.stroke()
        GlobalVariables.c.closePath()

        GlobalVariables.c.beginPath()
        GlobalVariables.c.fillStyle = '#949294'
        GlobalVariables.c.moveTo(xInPixels - radiusInPixels/3, yInPixels + radiusInPixels/5 )
        GlobalVariables.c.lineTo(xInPixels + radiusInPixels/3, yInPixels + radiusInPixels/5)
        GlobalVariables.c.stroke()
        GlobalVariables.c.closePath()



    }
    
    /**
     * Generate a layered outline of the part where the tool will cut
     */ 
    updateValue(){
        this.decreaseToProcessCountByOne()
        if(this.evolutionInProcess){
            this.updateSidebar()
            /**
             * Atom is processing
            * @type {boolean}
             */
            this.processing = true
            //Store the result from this individual in it's fitness value
            this.population[this.individualIndex].fitness = this.findIOValue('fitness function')
            
            //Evaluate the next individual
            this.individualIndex = this.individualIndex + 1
            if(this.individualIndex < this.findIOValue('population size')){
                //Evaluate the next individual by updating all of the inputs
                this.beginEvaluatingIndividual()
            }
            else{
                this.generation = this.generation + 1
                if(this.generation < this.findIOValue('number of generations')){
                    // Generate a new generation from the existing generation and start the process over
                    this.breedAndCullPopulation()
                    this.individualIndex = 0
                    this.beginEvaluatingIndividual()
                }
                else{
                    this.evolutionInProcess = false
                    this.processing = false
                    // Set the inputs to the prime candidate
                    this.population = this.population.sort((a, b) => parseFloat(b.fitness) - parseFloat(a.fitness))
                    this.individualIndex = 0
                    this.generation = 0
                    this.beginEvaluatingIndividual()
                    
                    this.updateSidebar()
                }
            }
        }
    }
    
    /**
     * Add a button to trigger the evolution process
     */ 
    updateSidebar(){
        
        if(this.evolutionInProcess){
            
            //Remove everything in the sidebar
            let sideBar = document.querySelector('.sideBar')
            while (sideBar.firstChild) {
                sideBar.removeChild(sideBar.firstChild)
            }
            
            //Generate the list
            var sbList = document.createElement('ul')
            sideBar.appendChild(sbList)
            sbList.setAttribute('class', 'sidebar-list')
            
            //Add text to the list element
            var listElement = document.createElement('LI')
            sbList.appendChild(listElement)
            
            var labelDiv = document.createElement('div')
            listElement.appendChild(labelDiv)
            var labelText = document.createTextNode("Evolving...   Individual: " + this.individualIndex + ",    Generation: " + this.generation + ",    Best Fitness Value: " + this.topFitness)
            labelDiv.appendChild(labelText)
            labelDiv.setAttribute('class', 'sidebar-subitem label-item')
        }
        else{
            
            var valueList =  super.updateSidebar() 
            
            this.createButton(valueList,this,'Evolve',() => {
                this.evolutionInProcess = true
                
                //Generate a list of all the constants to evolve
                this.updateConstantsList()
                
                //Generate a population from those constants
                this.initializePopulation()
                
                //Evaluate the first individual
                this.beginEvaluatingIndividual()
            })
        }
    }
    
    /**
     * Trigger the process to evaluate the current individual
     */ 
    beginEvaluatingIndividual(){
        const individualToEvaluate = this.population[this.individualIndex]
        
        //Lock all the constants
        individualToEvaluate.genome.forEach(gene => {
            gene.constantAtom.output.waitOnComingInformation()
        })
        
        //Set all of their values
        individualToEvaluate.genome.forEach(gene => {
            gene.constantAtom.output.value = gene.newValue
        })
        
        //Trigger them to update
        individualToEvaluate.genome.forEach(gene => {
            gene.constantAtom.updateValue()
        })
    }
    
    /**
     * Generate a random number between min and max
     */ 
    getRandomValue(min, max) {
        const randomVal = Math.random() * (max - min) + min
        return randomVal
    }
    
    /**
     * Generate an initial population
     */ 
    initializePopulation(){
        this.population = []
        this.individualIndex = 0
        this.generation = 0
        this.topFitness = 0
        
        var i = 0
        while(i < this.findIOValue('population size')){
            var genome = []
            this.constantsToEvolve.forEach(constant => {
                var gene = {
                    newValue: this.getRandomValue(constant.min, constant.max),
                    constantAtom: constant
                }
                genome.push(gene)
            })
            
            //Generate an individual with a random value for each input
            var individual = {
                genome: genome,
                fitness: null
            }
            this.population.push(individual)
            
            i++
        }
    }
    
    /**
     * Take two individuals and breed them to form a new individual with a mix of their genes and mutations
     */ 
    breedTwo(A, B){
        const lengthOfGenome = A.genome.length
        
        var child = {
            fitness: null,
            genome: []
        }
        
        var geneIndex = 0
        while(geneIndex < lengthOfGenome){
            var newGene = {
                newValue: null,
                constantAtom: A.genome[geneIndex].constantAtom
            }
            
            
            const maxVal         = A.genome[geneIndex].constantAtom.max
            const minVal         = A.genome[geneIndex].constantAtom.min
            const mutationAmount = 0.1*(maxVal-minVal)*(Math.random()-.5) //Mutate by at most +-.5%
            
            var newGeneVal = null
            if(Math.random() > 0.5){
                newGeneVal = A.genome[geneIndex].newValue+mutationAmount
            }else{
                newGeneVal = B.genome[geneIndex].newValue+mutationAmount
            }
            
            //Constrain to within bounds
            newGene.newValue = Math.min(Math.max(newGeneVal, minVal), maxVal)
            
            child.genome.push(newGene)
            
            geneIndex++
        }
        return child
    }
    
    /**
     * Breed the best performers in the population, cull the rest
     */ 
    breedAndCullPopulation(){
        this.population = this.population.sort((a, b) => parseFloat(b.fitness) - parseFloat(a.fitness))
        
        this.topFitness = this.population[0].fitness
        
        // Create a new population by taking the top 1/5th of the original population
        const keptPopulationNumber = Math.round(this.population.length / 5)
        
        const breeders = this.population.slice(0,keptPopulationNumber)
        
        //Generate a new population of individuals by breading from the last generation
        var newGeneration = []
        var index = 0
        while(index < this.population.length - keptPopulationNumber){
            const individualOneIndex = Math.round(Math.random()*(breeders.length-1))
            const individualTwoIndex = Math.round(Math.random()*(breeders.length-1))
            const newIndividual = this.breedTwo(breeders[individualOneIndex], breeders[individualTwoIndex])
            newGeneration.push(newIndividual)
            index = index + 1
        }
        
        this.population = breeders.concat(newGeneration)
    }
    
    /**
     * Regenerate the list of constants we are evolving
     */ 
    updateConstantsList(){
        //Create an array of the inputs by walking up stream
        this.constantsToEvolve = []
        this.inputs.forEach(input => {
            input.connectors.forEach(connector => {
                connector.walkBackForConstants(constantObject => {this.addToConstantsList(constantObject)})
            })
        })
    }
    
    /**
     * Add a constant to the list. Used as a callback from passing up the tree.
     */ 
    addToConstantsList(constantObject){
        this.constantsToEvolve.push(constantObject)
    }
    
}