population.js

const _ = require("lodash");
let Network = require('./architecture/network');
let methods = require('./methods/methods');
let config = require('./config');

// Easier variable naming
let selection = methods.selection;

function Population({
  template,
  size=50,
  data=[],
  population=[],
  fitness=(genome, data) => 1 - genome.test(data).error,
  _sorted=false,
  _selection=methods.selection.POWER
} = {}) {
  let self = this;

  _.assignIn(self, {template,size,data,population,fitness});

  if(self.template && !self.population.length) _.times(self.size, function() {
    self.population.push(Network.fromJSON({ ...self.template.toJSON(), score: undefined }));
  });
}

Population.prototype = {
  evaluate: async function() {
    let self = this;

    _.each(self.population, function(genome, index) {
      self.population[index].score = self.fitness(genome, self.data);
    })
  },

  // Mates 2 "good genomes"; returns child
  mate: function(genomes, options) {
    let self = this;
    return Network.crossOver(self.getParent(), self.getParent(), true);
  },

  // Selects a "good genome" by `this.selection`
  select: function (options) {
    let self = this;

    let i;
    switch (this.selection) {
      case selection.POWER:
        if(!self._sorted) self.population = _.sortBy(self.population, ["score"]);

        return this.population[Math.floor(Math.pow(Math.random(), self.selection.power) * self.population.length)];
      case selection.FITNESS_PROPORTIONATE:
        // As negative fitnesses are possible
        // https://stackoverflow.com/questions/16186686/genetic-algorithm-handling-negative-fitness-values
        // this is unnecessarily run for every individual, should be changed

        let totalFitness = 0;
        let minimalFitness = 0;
        for (i = 0; i < this.population.length; i++) {
          let score = this.population[i].score;
          minimalFitness = score < minimalFitness ? score : minimalFitness;
          totalFitness += score;
        }

        minimalFitness = Math.abs(minimalFitness);
        totalFitness += minimalFitness * this.population.length;

        let random = Math.random() * totalFitness;
        let value = 0;

        for (i = 0; i < this.population.length; i++) {
          let genome = this.population[i];
          value += genome.score + minimalFitness;
          if (random < value) return genome;
        }

        // if all scores equal, return random genome
        return this.population[Math.floor(Math.random() * this.population.length)];
      case selection.TOURNAMENT:
        if (this.selection.size > this.population_size) {
          throw new Error('Your tournament size should be lower than the population size, please change methods.selection.TOURNAMENT.size');
        }

        // Create a tournament
        let individuals = [];
        for (i = 0; i < this.selection.size; i++) {
          let random = this.population[Math.floor(Math.random() * this.population.length)];
          individuals.push(random);
        }

        // Sort the tournament individuals by score
        individuals.sort(function (a, b) {
          return b.score - a.score;
        });

        // Select an individual
        for (i = 0; i < this.selection.size; i++) {
          if (Math.random() < this.selection.probability || i === this.selection.size - 1) {
            return individuals[i];
          }
        }
    }
  },

  test: function(dataset, options) {},

  /**
   * @typedef {Object} state
   * @prop {number[]} state.information
   * @prop {number[]} state.action
   * @prop {number[]} state.reward
   */

  /**
   * @typedef {Object} data
   * @prop {number[]} data.input
   * @prop {number[]} data.output
   */

  /**
   * @typedef {data[]|state[]} Dataset
   */

  /**
   * @typedef {Object} EvolutionaryPeriod
   * @prop {number} generations
   * @prop {number} time
   * @prop {number} error
   * @prop {Object} networks
   * @prop {Network} networks.best
   * @prop {Network} networks.worst
   * @prop {Network[]} networks.all
   */

  /**
   * @param {number} [options.error=0.1] Target error for networks in population - _`evolve()` will stop running when ONE network reaches `options.error`, or `options.iterations` is reached_
   * @param {number} [options.iterations] Maximum number of generations over which population evolves - _`evolve()` will stop running if `options.error` is reached before `options.iterations`_
   * @param {number} [options.elitism=0.05] Percentage of networks copied into next generations (epoch) - _larger `elitism` leads to **faster learning**, but **lower accuracy**_
   * @param {number} [options.provenance=0] Percentage of "stem networks" (`this.template`) introduced into new generations (epoch) - _larger `provenance` leads to **slower learning**, but **higher accuracy**_
   * @param {number} [options.mutation.rate=0.05] Percentage of networks genomically mutated (changed) per generation (epoch)
   * @param {number} [options.mutation.amount=1] Number of genetic mutations (changes) introduced into mutated networks per generation (epoch)
   * @param {Array<methods.mutation>} [options.mutation.methods=methods.mutation.FFW] List of allowed genetic mutations (changes) to a network per generation (epoch)
   * @param {boolean} [options.networks.train=false] Trains every network in the populus, every generation - _VERY SLOW_
   * @param {boolean} [options.networks.evolve=false] Evolves every network in the populus, every generation - _VERY SLOW_
   * @param {number} [options.nodes.max=Infinity] Maximum number of nodes allowed in a network
   * @param {number} [options.nodes.min=0] Minimum number or nodes allowed in a network
   * @param {number} [options.connections.max=Infinity] Maximum number of connections allowed in a network
   * @param {number} [options.connections.min=0] Minimum number or connections allowed in a network
   * @param {number} [options.gates.max=Infinity] Maximum number of gates allowed in a network
   * @param {number} [options.gates.min=0] Minimum number or gates allowed in a network
   * @param {number} [options.log] Will log the populations performance every `options.log` generations as it evolves - _`options.log = true` will default to `options.log = 1`_
   * @param {number} [options.threads] Specifies the number of threads that can be used to train/evolve networks/populations - _defaults to the number of CPU cores
   * @param {number} [options.schedule.iterations] Frequency with which `options.schedule.function` is run
   * @param {Function} [options.schedule.function] A function that will run every `options.schedule.iterations`
   *
   * @returns {EvolutionaryPeriod}
   */
  evolve: async function(dataset, options) {
    let self = this;
    // OVERLOADS: Population.prototype.evolve(options)
    if(!options && _.isPlainObject(dataset)) {
      options = dataset;
      dataset = undefined;
    }

    // OVERLOADS: Population.prototype.evolve()
    // OVERLOADS: Population.prototype.evolve(options)
    // OVERLOADS: Population.prototype.evolve((undefined || null),options)
    if(!dataset && self.dataset.length) {
      dataset = self.dataset;
    }

    // CASE: No dataset; Can not train population.
    else {
      throw new ReferenceError(`Parameter "dataset" was not passed & "this.dataset" was not declared; can not train "Population" without parameter "dataset" or "this.dataset".`)
    }


  }
}

module.exports = Population;