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:

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:

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))