Bred to die

Once every while, some creationist will come along who seems to believe that the fact that animals and humans die represents evidence against evolution. The “logic” is that natural selection should inevitably favour longevity/potential immortality since an infinite lifetime means infinite possibilities for reproduction. I’ve always considered this “proof” a proof of the person’s mathematical illiteracy and little else – after all, with any non-zero death rate, the chance to actually survive that long converges to zero with longish finite lifespans, and so does the benefit from potential immortality.

The last time this argument came up, I used the opportunity to practice my new programming skills, so here’s a little script that simulates mortals and “immortals” evolving within the same population. You can play with the parameters, such as extrinsic death rate and rate of reproduction, and decide whether there is a trade-off to (potentially) living longer in terms of slower maturation (there should be, because that’s what we observe in real animals), and whether there’s a bias in life-shortening vs. life-prolonging mutations (there should be, again). We’re pretending that (potential) immortality is actually physically possible for complex organisms, and in fact that once you’re able to survive 160 years, you might as well be immortal.

Here’s what you get when you run the model with biased mutation rates (.003 for life shortening mutations vs. .001 for life-prolonging ones), and no trade-off, and a rate of reproduction of 0.25 per adult per year. We see that there’s a lot of noise, but on the long run the type which lives up to 57 or so years carries the day. Beyond that age, being hypothetically able to live longer doesn’t carry enough of a benefit for selection to overcome the direction of drift when you aren’t going to live that long anyway:

conflated_youngsters

And here’s what you get when you include a very moderate trade-off: The most-short-lived individuals reach maturity at 4 (1 year before they’re bound to die), while the immortals reach it at six, and intermediate degrees of longevity have varying chances of joining the club when they’re in between. In effect, this represents about 10% later maturation per doubling of lifespan, which looks like a very fair deal, but alas this is enough to clearly favour the shorter lived variants over immortals:

percentages_tradeoffandbias

Code below fold. Don’t tell me it’s slow, but feel free to tell me how to make it faster:

def longevity_selection2(extrinsic_mortality, rep_rate=1, del_rate=.001, ben_rate=.0001, population=500, limit=20000, benefit=(4,6)):
    """Model evolution of longevity; input parameters:
    extrinsic mortality, background death rate at prime age,
      does not include age related deaths or deaths from culling
      the population back to carrying capacity.
    rep_rate: rate of reproduction: # offspring per adult per year
    del_rate: rate of deleterious, life-shortening mutations,
      each reducing the potential lifespan by a factor of 2^0.5
    ben_rate, rate of beneficial, life-lengthening mutations,
      each increasing the potential lifespan by a factor of 2*0.5
    population: population size
    limit: years until simulation breaks off
    benefit: the ages when the most long-lived and the most short
      lived variants in the population reach maturity, to simulate
      a trade-off between longevity and fitness in young age, inter-
      mediate types have varying probabilities to reach maturity 
      between those ages; equal numbers -> no trade-off"""
    
    freqs = [] # frequency table for output
    from random import shuffle, randrange, uniform, sample
    from itertools import cycle
    dr = extrinsic_mortality
    bonus = benefit[1]-benefit[0]
    sr = 1-dr # annual survival rate 1 - death rate
    
    # initiate population, consisting of tuples of format 
    # (potential lifespan, year of birth):
    pop = [(randrange(11), # random longevity index,
                  # values range from 0 (death at 5)
                  # to 10 (mathematically death at 160, but
                  # treated as immortal
        randrange(-(int(benefit[1]*1.5) # random age
        ),0)) for i in range(population)]
    
    for gen_counter in range(limit):
        
        # deaths:
        # ambient death rate + age related deaths:
        for individual in pop:
            # death if older than the maximum age according 
            # to formula 5 * 2^(longevity_index/2)
            # longevity index 10 (which should be 160 years
            # explicitly exempt:
            if (individual[0] < 10 and gen_counter - 
               individual[1] > 5 * 2**(individual[0]*.5)) or \
               uniform(0,1) <= dr: # random extrinsic deaths, no exceptions
                pop.remove(individual)
        # culling the population back to 'population' if applicable
        # density related deaths are random
        if len(pop) > population:
            for individual in sample(pop, len(pop)-population):
                pop.remove(individual)
                
        
        # mutate the longevity gene(s) when generating offspring:
        def mutate(longev):
            # determine in which direction to mutate ('priority') if a 
            # mutation is to occur (random number 'score' below 
            # respective mutation rate)
            score, priority = uniform(0,1), randrange(2)
            if score <= ben_rate * 2 and priority == 0 and longev < 10:
                return longev + 1
            elif score <= del_rate * 2 and priority == 1 and longev > 0:
                return longev - 1
            else:
                return longev
        
        
        # births:
        shuffle(pop)
        """Add new individuals until population size limit is reached
        individuals consist of an extant individual's (potentially 
        mutated) longevity index and the current time stamp."""
        parents = [ # create sublist of pop of mature individuals only
                    ind for ind in pop if 
                        gen_counter-ind[1] >= benefit[1] # upper age or higher included automatically
                    or 
                        (
                            gen_counter-ind[1] >= benefit[0] # lower age or higher probabilistically
                        and 
                          (
                            (
                                dr > 0 # special case 0 death rate below to avoid division-by-zero
                            and 
                                # include individual if random number is smaller than 
                                # individual's probability, given death rate, of being  older than 
                                # 2 + 0.2 * longevity_index, gives statistically 0.2 years 
                                # later maturation per degree of longevity
                                uniform(0,1) < sum([(((sr**bonus)**.1)**(j)-((sr**bonus)**.1)**(j+1))/(1-sr**bonus) for j in range(ind[0],10)])
                            ) 
                          or 
                            (
                                dr == 0 # special case death rate 0, individuals are
                                   # assumed to be uniformly distributed between ages
                                   # 2 and 4
                            and 
                                randrange(0,10) >= ind[0]
                            )
                          )
                        )
                ]
        
        # If no rate of reproduction is set, offspring is
        # generated until carrying capacity is reached:
        if rep_rate == 0:
            offspring_number = population - len(pop)
        
        # If rate of reproduction is set, number of
        # offspring is determined from number of eligible adults:
        else:
            offspring_number = int(len(parents) * rep_rate)
        parents == cycle(parents)
        
        for i in range(offspring_number):
                    pop.append((mutate(parents[i][0]), gen_counter))
                    
        # store current frequencies of all variants
        freqs.append(tuple([[ind[0] for ind in pop].count(j) 
                     for j in range(11)]))
        
        # track progress through console output every 100 generations
        if gen_counter % 100 == 0:
            print gen_counter, freqs[-1], "population after reproduction:", len(pop)
    return (freqs, 1 - float(population)/len(pop))

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s