neat.js

const _ = require(`lodash`);
// const parameter = require(`./util/parameter`);
const Network = require(`./architecture/network`);
const methods = require(`./methods/methods`);
const config = require(`./config`);

/**
* Runs the NEAT algorithm on group of neural networks.
*
* @constructs Neat
*
* @param {number} [inputs=1] Size of input layer of the networks in the population
* @param {number} [outputs=1] Size of output layer of the networks in the population
* @param {Array<{inputs:number[],outputs:number[]}>} [dataset] Dataset used to train networks in the population at first - _other sets of data can be passed to `neat.evolve()` after constuction_
* @param {Object} options **Configuration Options**
* @param {number} [options.population_size=50] Population size of each generation.
* @param {number} [options.elitism=1] Elitism of every evolution loop. [Elitism in genetic algortihtms.](https://www.researchgate.net/post/What_is_meant_by_the_term_Elitism_in_the_Genetic_Algorithm)
* @param {number} [options.provenance=0] Number of genomes inserted the original network template (Network(input,output)) per evolution.
* @param {number} [options.mutation_rate=0.4] Sets the mutation rate. If set to 0.3, 30% of the new population will be mutated. Default is 0.4.
* @param {number} [options.mutation_amount=1] If mutation occurs (randomNumber < mutation_rate), sets amount of times a mutation method will be applied to the network.
* @param {cost} [options.cost=cost.MSE]  Specify the cost function for the evolution, this tells a genome in the population how well it's performing. Default: methods.cost.MSE (recommended).
* @param {boolean} [options.equal=false] When true [crossover](Network.crossOver) parent genomes are assumed to be equally fit and offspring are built with a random amount of neurons within the range of parents' number of neurons. Set to false to select the "fittest" parent as the neuron amount template.
* @param {number} [options.clear=false] Clear the context of the population's nodes, basically reverting them to 'new' neurons. Useful for predicting timeseries with LSTM's.
* @param {number} [options.growth=0.0001] Set the penalty for large networks. Penalty calculation: penalty = (genome.nodes.length + genome.connectoins.length + genome.gates.length) * growth; This penalty will get added on top of the error. Your growth should be a very small number.
* @param {number} [options.amount=1] Set the amount of times to test the trainingset on a genome each generation. Useful for timeseries. Do not use for regular feedfoward problems.
* @param {boolean} [options.fitnessPopulation=false] Flag to return the fitness of a population of genomes. Set this to false to evaluate each genome inidividually.
* @param {Function} [options.fitness] - A fitness function to evaluate the networks. Takes a `dataset` and a `genome` i.e. a [network](Network) or a `population` i.e. an array of networks and sets the genome `.score` property
* @param {string} [options.selection=FITNESS_PROPORTIONATE] [Selection method](selection) for evolution (e.g. Selection.FITNESS_PROPORTIONATE).
* @param {Array} [options.crossover] Sets allowed crossover methods for evolution.
* @param {Network} [options.network=false] Network to start evolution from
* @param {number} [options.maxNodes=Infinity] Maximum nodes for a potential network
* @param {number} [options.maxConns=Infinity] Maximum connections for a potential network
* @param {number} [options.maxGates=Infinity] Maximum gates for a potential network
* @param {mutation[]} [options.mutation] Sets allowed [mutation methods](mutation) for evolution, a random mutation method will be chosen from the array when mutation occurs. Optional, but default methods are non-recurrent
*
* @prop {number} generation A count of the generations
* @prop {Network[]} population The current population for the neat instance. Accessible through `neat.population`
*
* @example
* const { Neat } = require("@liquid-carrot/carrot");
*
* // new Neat()
* let neat = new Neat()
*
* // new Neat(options)
* let neat = new Neat({ population_size: 100 })
*
* // new Neat(dataset)
* let neat = new Neat([
*   { input: [0, 0], output: [0] },
*   { input: [0, 1], output: [1] },
*   { input: [1, 0], output: [1] },
*   { input: [1, 1], output: [0] }
* ])
*
* // new Neat(input, output)
* let neat = new Neat(64, 10)
*
* // new Neat(dataset, options)
* let neat = new Neat([
*   { input: [0, 0], output: [0] },
*   { input: [0, 1], output: [1] },
*   { input: [1, 0], output: [1] },
*   { input: [1, 1], output: [0] }
* ], { population_size: 100 })
*
* // new Neat(input, output, options)
* let neat = new Neat(64, 10, { population_size: 100 })
*
* // new Neat(input, output, dataset)
* let neat = new Neat(2, 1, [
*   { input: [0, 0], output: [0] },
*   { input: [0, 1], output: [1] },
*   { input: [1, 0], output: [1] },
*   { input: [1, 1], output: [0] }
* ])
*
* // new Neat(input, output, dataset, options)
* let neat = new Neat(2, 1, [
*   { input: [0, 0], output: [0] },
*   { input: [0, 1], output: [1] },
*   { input: [1, 0], output: [1] },
*   { input: [1, 1], output: [0] }
* ], { population_size: 100 })
*
*/
const Neat = function(inputs, outputs, dataset, options) {
  const self = this;

  // new Neat(dataset) || new Neat(options)
  if(!(outputs || dataset || options)) {
    if(_.isPlainObject(inputs)) options = inputs;
    else if(Array.isArray(inputs)) dataset = inputs;

    inputs = undefined;
  }

  // new Neat(dataset, options)
  else if(!(dataset || options) && Array.isArray(inputs) && _.isPlainObject(outputs)) {
    dataset = inputs;
    options = outputs;
    inputs = outputs = undefined;
  }

  // new Neat(input, output, options)
  else if(!(options) && _.isInteger(inputs) && _.isInteger(outputs) && _.isPlainObject(dataset)) {
    options = dataset;
    dataset = undefined;
  }

  // new Neat()
  // new Neat(population) - leave out for now
  // new Neat(input, output)
  // new Neat(population, options) - leave out for now
  // new Neat(population, dataset) - leave out for now
  // new Neat(input, output, dataset)
  // new Neat(population, dataset, options) - leave out for now
  // new Neat(input, output, dataset, options)
  inputs = inputs || 1;
  outputs = outputs || 1;
  dataset = dataset || [];
  options = _.defaultsDeep(options, Neat.default.options);
  options.template = options.template || new Network(inputs, outputs);

  Object.assign(self, { inputs, outputs, dataset, ...options});

  /**
   * Create the initial pool of genomes
   *
   * @function createPool
   *
   * @deprecated
   *
   * @memberof Neat
   *
   * @param {Network} network
   * @param {Number} population_size the number of types the genome of network will be copied to make the pool
   */
  self.createPool = function createPool(network, population_size) {
    const population = [];

    for (let i = 0; i < population_size; i++) {
      population.push(Network.fromJSON({ ...network.toJSON(), score: undefined }));
    }

    return population;
  };

  /**
   * Creates a new population
   *
   * @function createPopulation
   *
   * @alpha
   *
   * @memberof Neat
   *
   * @param {Network} [network] - Template network used to create population - _other networks will be "identical twins"_ - _will use `this.template`, if `network` is not defined_
   * @param {number} [size=50] - Number of network in created population - _how many identical twins created in new population_
   *
   * @returns {Network[]} Returns an array of networks
   */
  self.createPopulation = function create_networks_for_evolution(network, size) {
    if(!size && Number.isInteger(network)) {
      size = network;
      network = undefined;
    }

    const population = [];

    network = network ? network.clone() : (self.template || new Network(self.inputs, self.outputs));
    size = size || self.population_size;

    for(let index = 0; index < size; index++) {
      population.push(network);
    }

    return population;
  };

  // Initialise the genomes
  self.population = self.population || self.createPopulation(self.template, self.population_size);

  /**
   * Replaces all networks that match the `select` function - _if `transform` is provided networks will be transformed before being filtered out_
   *
   * @function replace
   *
   * @memberof Neat
   *
   * @param {Network[]} [population] An array (population) of genomes (networks)
   * @param {number|Network|Function} [filter] An index, network, or function used to pick out replaceable genome(s) from the population - _invoked `filter(network, index, population)`_
   * @param {Network|Function} [transform] A network used to replace filtered genomes or a function used to mutate filtered genomes - _invoked `transform(network, index, population)`_
   *
   * @return {Network[]} Returns the new genome
   */
  self.replace = function(population, filter, transform) {
    if(population == undefined && filter == undefined && transform == undefined) throw new ReferenceError("Missing required parameter 'transform'")
    
    function _transform(t) {
      const transformer = t instanceof Network ? (() => t) : typeof t === "function" ? t : new TypeError(`Expected ${t} to be a {Network|Function}`);
      return transformer;
    }
    function _filter(f) {
      const filter = Number.isFinite(f) ? (network, index, population) => index === f
        : f instanceof Network ? (network, index, population) => network === f
        : typeof f === "function" ? f
        : f == undefined ? () => true
        : new TypeError(`Expected ${t} to be a {Number|Network|Function|undefined}`);
      return filter;
    }
    
    if (filter == undefined && transform == undefined) {
      transform = _transform(population);
      filter = _filter();
      population = self.population;
    }
    else if (transform == undefined) {
      transform = _transform(filter);
      filter = _filter(population);
      population = self.population;
    } else {
      transform = _transform(transform);
      filter = _filter(filter);
      population = population || self.population;
    }
    
    const filtered = [...self.population];

    for (let genome = 0; genome < population.length; genome++)
      if (filter(population[genome], genome, population))
        filtered[genome] = transform(population[genome], genome, population);

    return filtered;
  };

  /**
   * Selects a random mutation method for a genome and mutates it
   *
   * @function mutateRandom
   *
   * @beta
   *
   * @memberof Neat
   *
   * @param {Network} genome Network to test for possible mutations
   * @param {mutation[]} allowedMutations An array of allowed mutations to pick from
   *
   * @return {mutation} Selected mutation
  */
  self.mutateRandom = function apply_random_mutation_method_to_genome(genome, allowedMutations) {
    let possible = allowedMutations ? [...allowedMutations] : [...self.mutation]

    // remove any methods disallowed by user-limits: i.e. maxNodes, maxConns, ...
    possible = possible.filter(function(method) {
      return (
        method !== methods.mutation.ADD_NODE || genome.nodes.length < self.maxNodes ||
        method !== methods.mutation.ADD_CONN || genome.connections.length < self.maxConns ||
        method !== methods.mutation.ADD_GATE || genome.gates.length < self.maxGates
      )
    })

    do {
      const current = possible[Math.floor(Math.random() * possible.length)]

      // attempt mutation, success: return mutation method, failure: remove from possible methods
      if (genome.mutate(current)) {
        return current;
      } else {
        possible = possible.filter(function(method) { return method.name !== current.name });
      }

      // Return null when mutation is impossible
      if (!possible || possible.length === 0) return null;

    } while(true)
  };

  /**
   * Mutates the given (or current) population
   *
   * @function mutate
   *
   * @memberof Neat
   *
   * @param {mutation} [method] A mutation method to mutate the population with. When not specified will pick a random mutation from the set allowed mutations.
   */
  self.mutate = function mutate_population(method) {
    if (method) {
      for (let i = 0; i < self.population.length; i++) { // Elitist genomes should not be included
        if (Math.random() <= self.mutation_rate) {
          for (let j = 0; j < self.mutation_amount; j++) {
            self.population[i].mutate(method);
          }
        }
      }
    } else {
      for (let i = 0; i < self.population.length; i++) { // Elitist genomes should not be included
        if (Math.random() <= self.mutation_rate) {
          for (let j = 0; j < self.mutation_amount; j++) {
            self.mutateRandom(self.population[i], self.mutation);
          }
        }
      }
    }
  };
  
  /**
   * Evaluates, selects, breeds and mutates population
   *
   * @memberof Neat
   *
   * @alias evolve
   *
   * @param {Array<{input:number[],output:number[]}>} [evolve_dataset=dataset] A set to be used for evolving the population, if none is provided the dataset passed to Neat on creation will be used.
   * @param {Function} pickGenome
   * @param {Function} filterGenome
   *
   * @returns {Object}
   *
   * @example
   *
   * // original
   * let originalSet = [
   *  { input: [0,0], output: [0] },
   *  { input: [0,1], output: [1] },
   *  { input: [1,0], output: [1] },
   *  { input: [1,1], output: [0] },
   * ]
   *
   * let neat = new Neat(originalSet, {
   *  input: 1,
   *  output: 2
   * });
   *
   * // special set to be used when evolving
   * let evolve_dataset = [
   *  { input: [0], output: [1] },
   *  { input: [1], output: [0] }
   * ]
   *
   * // evolves using evolve_dataset INSTEAD of originalSet
   * neat.evolve(evolve_dataset)
   *
   * // evolves using originalSet
   * neat.evolve()
   *
   * let pick = function pickGenome(genome) return genome.nodes.length > 100 ? true : false // Remove genomes with more than 100 nodes
   *
   * let adjust = function adjustGenome(genome) return genome.clear() // clear the nodes
   *
   * // evolves using originalSet
   * neat.evolve(null, filter, adjust)
   *
   */
  self.evolve = async function(evolve_dataset, pickGenome, filterGenome) {
    /*
    // // Check if evolve is possible
    // if (self.elitism + self.provenance > self.population_size) throw new Error("Can`t evolve! Elitism + provenance exceeds population size!");

    // dataset = dataset || self.dataset;
    
    
    // // Reset the scores
    // for (let i = 0; i < self.population.length; i++) self.population[i].score = undefined;

    // // Check population for evaluation
    // // if (typeof self.population[self.population.length - 1].score === `undefined`)
    // await self.evaluate(dataset);
    
    // console.log(self.population[0].score);

    // // Sort in order of fitness (fittest first)
    // self.sort();

    // // Elitism, assumes population is sorted by fitness
    // const elitists = [];
    // for (let i = 0; i < self.elitism; i++) elitists.push(self.population[i]);

    // // Provenance
    // const new_population = Array(self.provenance).fill(Network.fromJSON(self.template.toJSON()))

    // // Breed the next individuals
    // for (let i = 0; i < self.population_size - self.elitism - self.provenance; i++)
    //   new_population.push(self.getOffspring());

    // // Replace the old population with the new population
    // self.population = new_population;

    // // Mutate the new population
    // self.mutate();

    // // Add the elitists
    // self.population.push(...elitists);

    // // evaluate the population
    // await self.evaluate(dataset);

    // // Sort in order of fitness (fittest first)
    // self.sort()

    // const fittest = Network.fromJSON(self.population[0].toJSON());
    // fittest.score = self.population[0].score;

    // const best = Network.fromJSON(self.population[0].toJSON());
    // best.score = self.population[0].score
    // const worst = Network.fromJSON(self.population[self.population.length - 1].toJSON());
    // worst.score = self.population[self.population.length - 1].score
    // const median = Network.fromJSON(self.population[Math.floor(self.population.length / 2)].toJSON());
    // median.score = self.population[Math.floor(self.population.length / 2)].score


    // self.generation++;

    // if (options && options.networks) {
    //   return { best, median, worst }
    // } else {
    //   return {
    //     best: best.score,
    //     median: median.score,
    //     worst: worst.score
    //   }
    // }
    
    //==========================================================
    
    // console.log(dataset);
    // console.log(options);
    
    // if (options == undefined && !Array.isArray(dataset) && typeof dataset === "object") {
    //   options = dataset;
    //   dataset = self.dataset;
    // }
    
    // options = options || {};
    // dataset = dataset || self.dataset || [];
    
    
    // console.log(dataset);
    // console.log(options);
    
    // console.log(dataset == undefined);
    // console.log(!dataset.length);
    
    // if (dataset == undefined || !dataset.length) throw new ReferenceError("'dataset' was not passed to 'neat.evolve()' or 'new Neat'");
    
    
    
    // for (let index = 0; index < self.population.length; index++) {
    //   await self.population[index].evolve(dataset);
    // }
    
    // const best = self.population[0];
    // const worst = self.population[self.population.length - 1];
    // const median = self.population[Math.floor(self.population.length / 2)];
    
    // if (options && options.networks) {
    //   return { best, median, worst }
    // } else {
    //   return {
    //     best: best.score,
    //     median: median.score,
    //     worst: worst.score
    //   }
    // }
    
    //==========================================================
    */
    
    // Check if evolve is possible
    if (self.elitism + self.provenance > self.population_size) {
      throw new Error(`Can't evolve! Elitism + provenance exceeds population size!`);
    }

    // evolve dataset is optional, so deal with not having it
    if (typeof evolve_dataset === `function`) {
      adjustGenome = pickGenome;
      pickGenome = evolve_dataset
      evolve_dataset = undefined;
    }

    evolve_dataset = evolve_dataset || self.dataset;

    // Check population for evaluation
    if (self.population[self.population.length - 1].score == undefined) {
      await self.evaluate(evolve_dataset);
    }
    // Check & adjust genomes as needed
    if (pickGenome) {
      self.population = self.filterGenome(self.population, self.template, pickGenome, adjustGenome);
    }

    // Sort in order of fitness (fittest first)
    self.sort();

    // Elitism, assumes population is sorted by fitness
    const elitists = [];
    for (let index = 0; index < self.elitism; index++) elitists.push(self.population[index]);

    // Provenance
    const new_population = Array(self.provenance).fill(Network.fromJSON(self.template.toJSON()))

    // Breed the next individuals
    for (let i = 0; i < self.population_size - self.elitism - self.provenance; i++) {
      new_population.push(self.getOffspring());
    }

    // Replace the old population with the new population
    self.population = new_population;

    // Mutate the new population
    self.mutate();

    // Add the elitists
    self.population.push(...elitists);

    // evaluate the population
    await self.evaluate(evolve_dataset);

    // Check & adjust genomes as needed
    if (pickGenome) self.population = self.filterGenome(self.population, self.template, pickGenome, adjustGenome)

    // Sort in order of fitness (fittest first)
    self.sort()

    const fittest = Network.fromJSON(self.population[0].toJSON());
    fittest.score = self.population[0].score;

    // Reset the scores
    for (let i = 0; i < self.population.length; i++) self.population[i].score = undefined;

    self.generation++;

    return fittest;
  };

  /**
   * Returns a genome for recombination (crossover) based on one of the [selection methods](selection) provided.
   *
   * Should be called after `evaluate()`
   *
   * @function getParent
   *
   * @memberof Neat
   *
   * @return {Network} Selected genome for offspring generation
   */
  self.getParent = function get_genome_using_selection_method() {
    switch (self.selection.name) {
      case `POWER`: {
        if (self.population[0].score < self.population[1].score) self.sort();

        const index = Math.floor(Math.pow(Math.random(), self.selection.power) * self.population.length);
        return self.population[index];
      }
      case `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 total_fitness = 0;
        let minimum_fitness = 0;
        for (let i = 0; i < self.population.length; i++) {
          const score = self.population[i].score;
          minimum_fitness = score < minimum_fitness ? score : minimum_fitness;
          total_fitness += score;
        }

        minimum_fitness = Math.abs(minimum_fitness);
        total_fitness += minimum_fitness * self.population.length;

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

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

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

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

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

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

  /**
   * Selects two genomes from the population with `getParent()`, and returns the offspring from those parents. NOTE: Population MUST be sorted
   *
   * @function getOffspring
   *
   * @memberof Neat
   *
   * @returns {Network} Child network
   */
  self.getOffspring = function() {
    const parent1 = self.getParent();
    const parent2 = self.getParent();

    return Network.crossOver(parent1, parent2, self.equal);
  };

  /**
   * Evaluates the current population, basically sets their `.score` property
   *
   * @function evaluate
   *
   * @memberof Neat
   *
   * @param {Object[]} [dataset]
   * @param {Object} [options]
   * @param {boolean} [options.clear=false]
   * @param {boolean} [options.networks=false]
   *
   * @return {Object} Return the performance metrics/benchmarks of the networks - _returns networks iff `options.networks === true`_
   */
  self.evaluate = async function (dataset) {
    dataset = dataset || self.dataset;

    if (self.fitnessPopulation) {
      // check the clear flag
      if (self.clear) {
        for (let i = 0; i < self.population.length; i++) {
          self.population[i].clear();
        }
      }

      // calculate the fitnesses
      await self.fitness(dataset, self.population);
    } else {
      for (let i = 0; i < self.population.length; i++) {
        const genome = self.population[i];
        if (self.clear) genome.clear(); // clear flag
        genome.score = await self.fitness(dataset, genome);
        self.population[i] = genome;
      }
    }

    // Sort the population in order of fitness
    self.sort()

    // return the fitness of the best agent, which represents the fitness of the population
    return self.population[0]
  };

  /**
   * Sorts the population by score
   *
   * @function sort
   *
   */
  self.sort = function sort_population_by_fitness() {
    self.population.sort(function (a, b) {
      return b.score - a.score;
    });
  };

  /**
   * Returns the fittest genome of the current population
   *
   * @function getFittest
   *
   * @memberof Neat
   *
   * @returns {Network} Current population's fittest genome
  */
  self.getFittest = function get_fittest_population_genome() {
    // Check if evaluated. self.evaluate is an async function
    if (typeof self.population[self.population.length - 1].score === `undefined`) {
      self.evaluate();
    }

    if (self.population[0].score < self.population[1].score) self.sort();

    return self.population[0];
  };

  /**
   * Returns the average fitness of the current population
   *
   * @function getAverage
   *
   * @memberof Neat
   *
   * @returns {number} Average fitness of the current population
   */
  self.getAverage = function get_average_population_fitness() {
    if (typeof self.population[self.population.length - 1].score === `undefined`)
      self.evaluate(); // self.evaluate is an async function

    let score = 0;
    for (let i = 0; i < self.population.length; i++)
      score += self.population[i].score;

    return score / self.population.length;
  };

  /**
   * Export the current population to a JSON object
   *
   * Can be used later with `fromJSON(json)` to reload the population
   *
   * @function toJSON
   *
   * @memberof Neat
   *
   * @return {object[]} A set of genomes (a population) represented as JSON objects.
   */
  self.toJSON = function export_to_json() {
    const json = [];
    for (let i = 0; i < self.population.length; i++) {
      json.push(self.population[i].toJSON());
    }
    return json;
  };

  /**
   * Imports population from a json. Must be an array of networks converted to JSON objects.
   *
   * @function fromJSON
   *
   * @memberof Neat
   *
   * @param {object[]} json set of genomes (a population) represented as JSON objects.
  */
  self.fromJSON = function import_from_json(json) {
    const population = [];
    for (let i = 0; i < json.length; i++)
      population.push(Network.fromJSON(json[i]));
    self.population = population;
    self.population_size = population.length;
  };
}

Neat.default = {
  options: {
    generation: 0, // internal variable
    // input: 1,
    // output: 1,
    equal: true,
    clean: false,
    population_size: 50,
    growth: 0.0001,
    cost: methods.cost.MSE,
    amount: 1,
    elitism: 1,
    provenance: 0,
    mutation_rate: 0.4,
    mutation_amount: 1,
    fitnessPopulation: false,
    fitness: function(set = dataset, genome, amount = 1, cost = methods.cost.MSE, growth = 0.0001) {
      let score = 0;
      for (let i = 0; i < amount; i++) score -= genome.test(set, cost).error;

      score -= (genome.nodes.length - genome.input - genome.output + genome.connections.length + genome.gates.length) * growth;
      score = isNaN(score) ? -Infinity : score; // this can cause problems with fitness proportionate selection

      return score / amount;
    },
    selection: methods.selection.POWER,
    crossover: [
      methods.crossover.SINGLE_POINT,
      methods.crossover.TWO_POINT,
      methods.crossover.UNIFORM,
      methods.crossover.AVERAGE
    ],
    mutation: methods.mutation.FFW,
    // template: new Network(this.input, this.output)
    maxNodes: Infinity,
    maxConns: Infinity,
    maxGates: Infinity
  }
}

module.exports = Neat;