2
\$\begingroup\$

Genetic drift — along with natural selection, mutation, and migration — is one of the basic mechanisms of evolution.

In each generation, some individuals may, just by chance, leave behind a few more descendents (and genes, of course!) than other individuals. The genes of the next generation will be the genes of the "lucky" individuals, not necessarily the healthier or "better" individuals. That, in a nutshell, is genetic drift. It happens to ALL populations — there's no avoiding the vagaries of chance.

Source

The following code calculates a pseudo-random likelyhood of event-sequences as part of Genetic drift. I know Python would probably be a better choice, and fit more with the console iteraction taking place, but, that's probably a question for another day.

It takes input in the form of an optional config array with three (combinationally optional) parameters:

  • gen: the amount of generations to execute (int input, or continuous)
  • population: a dictionary of populations, can look like:

    • {'Red Beetle': 50, 'Blue Beetle': 50, 'Green Beetle': 0, 'Purple Beetle': 50, 'Orange Beetle': 50}
    • {red: 50, blue: 50}
    • Or different combinations
  • draw_amount: the amount of population to test for the next generation

The logic follows a similar pattern to this:

  • Until either the generations result in a single population surviving or the counter is hit, continue looping over the following: (loop here)
    • take a dictionary form like {red: 2, blue: 2} and turn it into ['red', 'red', 'blue', 'blue] (add the key to an array value times) (double loop here)
    • uses Math.random to fetch draw_amount values from the recently populated array (loop here)
    • Prints result (loop here)
    • Converts that back into a dictionary (loop here)

I was worried about a few things:

  • Too many loops?
  • When converting back into an array, with a draw_amount that doesn't get mod 0 with the population
  • Whether the error handlers have been correctly used

var genetic_drift = function(user_config){
    var config = {};
    var default_config = {
            gen: 5,
            population: {red: 50, blue: 50},
            draw_amount: 10
    };
    (function(user_config){
        if (user_config){
            for (var item in default_config){
                user_config[item] ? '' : user_config[item] = default_config[item];
            }
        } else {
            user_config = default_config;
        }
        config = user_config;
    })(user_config);
    genetic_drift.begin = function(){
        var population = [];
        var ratio_population = config.population;
        var until = (config.gen == 'continuous');
        for (var i = 0, gen = (until ? 1 : config.gen); i < gen; i++){
            population = [];
            if (Object.keys(ratio_population).length == 1){
                console.log('Generation ' + i + ': One trait left: ' + Object.keys(ratio_population)[0]);
                break;
            }
            for (var item in ratio_population){
                for (var k = 0; k < ratio_population[item]; k++){
                    population.push(item);
                }
            }
            if (config.draw_amount >= population.length){
                throw ('SimulationError: The DrawAmount is more than the Population.');
            }
            ratio_population = process_generation(population, config.draw_amount, i+1);
            if (until){
                gen++;
            }
        }
    };

    function process_generation(population, draw_amount, gen){
        var temp_drawn = [];
        var temp_final = {};
        for (var i = 0; i < draw_amount; i++){
            temp_drawn.push(population[Math.floor(Math.random() * population.length)]);
        }
        if (population.length % draw_amount == 0){
            temp_drawn.forEach(function(item){
                for (var i = 0, multiplier = Math.floor(population.length / draw_amount); i < multiplier; i++){
                    temp_final[item] = (item in temp_final ? temp_final[item] + 1 : 1);
                }
            });
        }
        var print_result = "Generation " + gen + ": ";
        Object.keys(temp_final).forEach(function(key){
            print_result += [key, temp_final[key]].join(':') + ' '
        });
        console.log(print_result);
        return temp_final;
    };
};

Which then would be used like:

var config = {
    gen: 'continuous',
    population: {
        'Red Beetle': 50,
        'Blue Beetle': 50,
        'Green Beetle': 0,
        'Purple Beetle': 50,
        'Orange Beetle': 50
        },
    draw_amount: 20
};
genetic_drift(config);
genetic_drift.begin();

Resulting in something like this:

 Generation 1: Orange Beetle:70 Blue Beetle:70 Red Beetle:30 Purple Beetle:30 
 Generation 2: Orange Beetle:80 Purple Beetle:10 Red Beetle:40 Blue Beetle:70 
 Generation 3: Blue Beetle:60 Red Beetle:30 Orange Beetle:110 
 Generation 4: Red Beetle:30 Orange Beetle:110 Blue Beetle:60 
 Generation 5: Orange Beetle:120 Red Beetle:30 Blue Beetle:50 
 Generation 6: Orange Beetle:150 Red Beetle:40 Blue Beetle:10 
 Generation 7: Orange Beetle:150 Red Beetle:50 
 Generation 8: Orange Beetle:180 Red Beetle:20 
 Generation 9: Orange Beetle:180 Red Beetle:20 
 Generation 10: Orange Beetle:200 
 Generation 10: One trait left: Orange Beetle
\$\endgroup\$
4
\$\begingroup\$

There is no reason to wrap this inside a function:

(function(user_config){
    if (user_config){
        for (var item in default_config){
            user_config[item] ? '' : user_config[item] = default_config[item];
        }
    } else {
        user_config = default_config;
    }
    config = user_config;
})(user_config);

(EDITED: I didn't handle passing only a subset of the possible options, as @user5325596 pointed out)

The first 15 lines of the function can be simplified to just this:

var config = {
    gen: 5,
    population: {red: 50, blue: 50},
    draw_amount: 10
};
if (user_config) {
    for (var item in user_config) {
        config[item] = user_config[item];
    }
}

It's easier to start with the default configs and copy over all of the user_config, than to start with the user's config and check which configs should be copied and which not.

See updated note about Underscore at the end of this answer.


var genetic_drift = function(config){
    /* ... */
    genetic_drift.begin = function(){
        /* ... use config and call process_generation ... */
    };

    function process_generation(population, draw_amount, gen){
        /* ... */
    };
};

You made begin a property of the function genetic_drift. The preferred (Object-Oriented) solution is to make genetic_drift a constructor, and make begin and process_generation methods of the resulting object:

var GeneticDriftSimulation = function(config){
    this.config = config;
    /* ... */
};

GeneticDriftSimulation.prototype.begin = function() {
    /* ... use this.config and call this.process_generation ... */
};

GeneticDriftSimulation.prototype.process_generation = function(population, draw_amount, gen){
    /* ... */
};

Then it can be used like this:

var simulation = new GeneticDriftSimulation(config);
simulation.begin();

Renames: (other names are also possible)

  • begin should be run (it runs the whole simulation)
  • 'continuous' should be 'infinite' or 'indefinite'
  • config.gen should be config.num_generations or config.num_iterations
  • until should be isIndefinite (until is not a good name for a boolean)
  • draw_amount should be sample_size

var population = [];

population is initialized at the beginning of the loop, so you only need you to declare it inside the loop.


var isIndefinite = (this.config.num_generations == 'indefinite');
for (var i = 0, gen = (isIndefinite ? 1 : this.config.num_generations); i < gen; i++){
    /* ... use i here, gen is not used ... */
    if (isIndefinite){
        gen++;
    }
}

This is convoluted way to handle the indefinite case (which introduces an extra variable). I'm guessing you had the following code before you handled the indefinite case:

for (var i = 0; i < this.config.num_generations; i++) {
    /* ... use i here ... */
}

The best way to change this code to handle the indefinite case is to change to condition so it is always true in that case:

var isIndefinite = (this.config.num_generations == 'indefinite');
for (var i = 0; isIndefinite || i < this.config.num_generations; i++) {
    /* ... use i here ... */
}

EDIT: the important thing here is to not force the indefinite case to use (and work around) logic that is only useful to the definite case (the i < logic).


for (var item in ratio_population){
    for (var k = 0; k < ratio_population[item]; k++){
        population.push(item);
    }
}

This nested loop looks fine to me, but rename item to populationName (or some other descriptive name). Also, declare population right before this loop, as it's not used before that.


if (config.draw_amount >= population.length){
    throw ('SimulationError: The DrawAmount is more than the Population.');
}

The error handling is fine, but you don't need the parentheses after throw. Also, move this check to process_generation; it belongs with the code that uses config.draw_amount and population.length.


var temp_drawn = [];
var temp_final = {};
for (var i = 0; i < draw_amount; i++){
    temp_drawn.push(population[Math.floor(Math.random() * population.length)]);
}
if (population.length % draw_amount == 0){
    temp_drawn.forEach(function(item){
        for (var i = 0, multiplier = Math.floor(population.length / draw_amount); i < multiplier; i++){
            temp_final[item] = (item in temp_final ? temp_final[item] + 1 : 1);
        }
    });
}
  • The sampling code (first loop) looks fine.
  • If you can't handle the case where population.length % draw_amount !== 0, throw an exception.
  • No need to call Math.floor if you know it's an integer.
  • Declare multiplier before the loop so it doesn't obscure the var i = 0; i < multiplier; i++ logic.
  • Split this into two functions: one for sampling and one for counting. (The second one will take multiplier as a parameter`)

The inner line can be rewritten to split the incrementing and the initializing of new keys:

if(temp_final[item] === undefined)
    temp_final[item] = 0;

temp_final[item]++;

var print_result = "Generation " + gen + ": ";
Object.keys(temp_final).forEach(function(key){
    print_result += [key, temp_final[key]].join(':') + ' '
});
console.log(print_result);
  • Move this to run so that process_generation only does the computation and not the printing.
  • You can extract the loop into a new method of GeneticDriftSimulation.
  • I think the code will be easier to read if you don't use join, because there are only two elements in that array.

You can use Underscore.js, a library that provides many utilities that Javascript is missing. Particularly:

  • sample for the sampling
  • defaults for merging user_config with the default configs.

I also recommend looking at the other utilities Underscore provides, and see the annotated source for the implementation of these functions.

Good luck!

\$\endgroup\$
1
\$\begingroup\$

If the browsers you are targeting support Object.assign, you could use it to simplify your configuration.

var config = Object.assign({}, default_config, user_config);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.