Conditional operators

Conditional operator (operator IfElse) *

Operator IfElse provides a simple way to conditionally apply an operator. The condition can be a fixed condition, a expression (a string) that will be evaluated in a population’s local namespace or a user-defined function when it is applied to the population.

The first case is used to control the execution of certain operators depending on user input. For example, Example IfElseFixed determines whether or not some outputs should be given depending on a variable verbose. Note that the applicability of the conditional operators are determined by the IfElse operator and individual opearators. That is to say, the parameters begin, step, end, at, and reps of operators in ifOps and elseOps are only honored when operator IfElse is applied.

Example: A conditional opeartor with fixed condition

>>> import simuPOP as sim
>>> pop = sim.Population(size=1000, loci=1)
>>> verbose = True
>>> pop.evolve(
...     initOps=[
...         sim.InitSex(),
...         sim.InitGenotype(freq=[0.5, 0.5]),
...     ],
...     matingScheme=sim.RandomMating(),
...     postOps=sim.IfElse(verbose,
...         ifOps=[
...             sim.Stat(alleleFreq=0),
...             sim.PyEval(r"'Gen: %3d, allele freq: %.3f\n' % (gen, alleleFreq[0][1])",
...                 step=5)
...         ],
...         begin=10),
...     gen = 30
... )
Gen:  10, allele freq: 0.483
Gen:  15, allele freq: 0.455
Gen:  20, allele freq: 0.481
Gen:  25, allele freq: 0.481
30L

Download IfElseFixed.py

When a string is specified, it will be considered as an expression and be evaluated in a population’s namespace. The return value will be used to determine if an operator should be executed. For example, you can re-introduce a mutant if it gets lost in the population, output a warning when certain condition is met, or record the occurance of certain events in a population. For example, Example IfElse records the number of generations the frequency of an allele goes below 0.4 and beyong 0.6 before it gets lost or fixed in the population. Note that a list of else-operators can also be executed when the condition is not met.

Example: A conditional opeartor with dynamic condition

>>> import simuPOP as sim
>>> simu = sim.Simulator(
...     sim.Population(size=1000, loci=1),
...     rep=4)
>>> simu.evolve(
...     initOps=[
...         sim.InitSex(),
...         sim.InitGenotype(freq=[0.5, 0.5]),
...         sim.PyExec('below40, above60 = 0, 0')
...     ],
...     matingScheme=sim.RandomMating(),
...     postOps=[
...         sim.Stat(alleleFreq=0),
...         sim.IfElse('alleleFreq[0][1] < 0.4',
...             sim.PyExec('below40 += 1')),
...         sim.IfElse('alleleFreq[0][1] > 0.6',
...             sim.PyExec('above60 += 1')),
...         sim.IfElse('len(alleleFreq[0]) == 1',
...             sim.PyExec('stoppedAt = gen')),
...         sim.TerminateIf('len(alleleFreq[0]) == 1')
...     ]
... )
(892L, 1898L, 4001L, 2946L)
>>> for pop in simu.populations():
...     print('Overall: %4d, below 40%%: %4d, above 60%%: %4d' % \
...         (pop.dvars().stoppedAt, pop.dvars().below40, pop.dvars().above60))
...
Overall:  891, below 40%:   20, above 60%:  515
Overall: 1897, below 40%: 1039, above 60%:   51
Overall: 4000, below 40%: 2878, above 60%:    0
Overall: 2945, below 40%:  198, above 60%: 1731

Download IfElse.py

In the last case, a user-defined function can be specified. This function should accept parameter pop when the operator is applied to a population, and one or more parameters pop, off, dad and mom when it is applied during-mating. The later could be used to apply different during-mating operators for different types of parents or offspring. For example, Example pedigreeMatingAgeStructured in Chapter 6 uses a CloneGenoTransmitter when only one parent is available (when parameter mom is None), and a MendelianGenoTransmitter when two parents are available.

Conditionally terminate an evolutionary process (operator TerminateIf)

Operator TerminateIf has been described and used in several examples such as Example simuGen, expression and IfElse. This operator accept an Python expression and terminate the evolution of the population being applied if the expression is evaluated to be True. This operator is well suited for situations where the number of generations to evolve cannot be determined in advance.

If a TerminateIf operator is applied to the offspring generation, the evolutionary cycle is considered to be completed. If the evolution is terminated before mating, the evolutionary cycle is condered to be incomplete. Such a difference can be important if the number of generations that have been involved is important for your analysis.

A less-known feature of operator TerminateIf is its ability to terminate the evolution of all replicates, using parameter stopAll=True. For example, Example TerminateIf terminates the evolution of all populations when one of the populations gets fixed. The return value of simu.evolve shows that some populations have evolved one generation less than the population being fixed.

Example: Terminate the evolution of all populations in a simulator

>>> import simuPOP as sim
>>> simu = sim.Simulator(
...     sim.Population(size=100, loci=1),
...     rep=10)
>>> simu.evolve(
...     initOps=[
...         sim.InitSex(),
...         sim.InitGenotype(freq=[0.5, 0.5]),
...     ],
...     matingScheme=sim.RandomMating(),
...     postOps=[
...         sim.Stat(alleleFreq=0),
...         sim.TerminateIf('len(alleleFreq[0]) == 1', stopAll=True)
...     ]
... )
(88L, 88L, 88L, 88L, 87L, 87L, 87L, 87L, 87L, 87L)
>>>

Download TerminateIf.py

Conditionally revert an evolutionary process to a saved state (operator RevertIf)

Operator RevertIf is a very interesting operator. It accepts a condition and a saved population (.pop file) and will revert the current evolving population to the saved population if the condition is met.

For example, one of the biggest problem with introducing a disease allele to an evolving population (using an operator PointMutator) is that the disease allele will very likely get lost because of genetic drift. It is possible to simulate the allele frequency trajectory backward in time and follow the trajectory during the forward-time simulation phase (simuPOP.utils.simulateBackwardTrajectory, simuPOP.utils.simulateForwardTrajectory, and a ControlledRandomMating mating scheme with a ControlledOffspringGenerator). However, that method is applicable only to evolutionary processes with a small number of loci under selection, and has a number of limitations (e.g. unlinked disease predisposing loci).

A natural way to simulate the introduction of disease alleles is therefore to terminate and restart the simulation whenever the disease allele gets lost (or fixed). This could be done by splitting the evolutionary process into two stages. The disease allele is introduced at the second stage and the simulation will be terminated as soon as the introduced allele is lost (using a TerminateIf operator). The second stage would be repeated until the simulation succeeds. Alternatively, you can save the population before the introduction of the disease allele, and revert to the saved population when the introduced allele gets lost. The latter can be done using operator RevertIf.

Example RevertToSaved shows an example of such an evolutionary process. This example saves an evolving population at the beginning of generation 4 and introduces a disease allele to the population. Starting from the fifth generation, a RevertIf opeartor checks if the disease allele still exists in the population, and revert to the saved population if the allele has been lost. When you read the example, it is important to remind yourself that after the RevertIf operator is triggered and applied, the population is at generation 4 before the disease allele is introduced, This is why the SavePopulation and RevertIf operators are usually put together.

Example: Revert an evolutionary process to a previous saved state when an introduced allele is lost

>>> import simuPOP as sim
>>>
>>> pop = sim.Population(1000, loci=1)
>>> evolved = pop.evolve(
...     initOps=sim.InitSex(),
...     preOps=[
...         sim.SavePopulation('init.pop', at=4),
...         sim.RevertIf('alleleFreq[0][1] == 0', "init.pop", begin=5),
...         sim.PointMutator(at=4, inds=0, allele=1, loci=0),
...     ],
...     matingScheme=sim.RandomMating(),
...     postOps=[
...         sim.Stat(alleleFreq=0),
...         sim.PyEval(r"'%d %.4f\n' % (gen, alleleFreq[0][1])"),
...         ],
...     gen=20
... )
0 0.0000
1 0.0000
2 0.0000
3 0.0000
4 0.0000
4 0.0000
4 0.0005
5 0.0010
6 0.0005
7 0.0010
8 0.0015
9 0.0010
10 0.0010
11 0.0005
12 0.0010
13 0.0010
14 0.0030
15 0.0015
16 0.0010
17 0.0000
4 0.0000
4 0.0005
5 0.0000
4 0.0005
5 0.0005
6 0.0005
7 0.0010
8 0.0005
9 0.0010
10 0.0000
4 0.0005
5 0.0005
6 0.0020
7 0.0040
8 0.0020
9 0.0015
10 0.0025
11 0.0015
12 0.0030
13 0.0015
14 0.0040
15 0.0045
16 0.0040
17 0.0065
18 0.0070
19 0.0055
>>> print('Evolved {} generations'.format(evolved))
Evolved 46 generations

Download RevertToSaved.py

Although operator RevertIf can also be used to fast-forward an evolutionary process (skip to a previously saved state directly), it is tricky to make it work with complex demographic models and therefore not recommended.

Conditional during mating operator (operator DiscardIf)

Operator DiscardIf accepts a condition or a Python function. When it is applied during mating, it will evaluate the condition or call the function for each offspring, and discard the offspring if the return value of the expression or function is True. The python expression accepts information fields as variables so operator DiscardIf('age > 80') will discard all individuals with age > 80. Optionally, the offspring itself can be used in the expression if parameter exposeInd is used to set the variable name of the offspring.

Alternatively, a Python function can be passed to this operator. This function should be defined with parameters pop, off, mom, dad or names of information fields. For example, DiscardIf(lambda age: age > 80) will remove individuals with age > 80.

A constant expression is also allowed in this operator. Although it does not make sense to use DiscardIf(True) because all offspring will be discarded, it is quite useful to use this operator in the context of DiscardIf(True, subPops=[(0, 0)]) to remove all individuals in a virtual subpopulation. If virtual subpopulation (0, 0) is defined as all individuals with age > 80, the last method achieves the same effect as the first two methods.

Example DiscardIf demonstrates an interesting application of this operator. This example evolves a population for one generation. Instead of keeping all offspring, it keeps only 500 affected and 500 unaffected offspring. This is achieved by defining virtual subpopulations by affection status and range, and discard the first 500 offspring if they are unaffected, and the last 500 offspring if they are affected.

Example: Use operator DiscardIf to generate case control samples

>>> import simuPOP as sim
>>> pop = sim.Population(size=500, loci=1)
>>> pop.setVirtualSplitter(sim.ProductSplitter([
...     sim.AffectionSplitter(),
...     sim.RangeSplitter([[0,500], [500, 1000]]),
...     ])
... )
>>> pop.evolve(
...     initOps=[
...         sim.InitSex(),
...         sim.InitGenotype(freq=[0.5, 0.5]),
...     ],
...     matingScheme=sim.RandomMating(
...         ops=[
...             sim.MendelianGenoTransmitter(),
...             sim.MaPenetrance(loci=0, penetrance=[0, 0.01, 0.1]),
...             sim.DiscardIf(True, subPops=[
...                 (0, 'Unaffected, Range [0, 500)'),
...                 (0, 'Affected, Range [500, 1000)')])
...         ],
...         subPopSize=1000,
...     ),
...     gen = 1
... )
1L
>>> sim.stat(pop, numOfAffected=True)
>>> print(pop.dvars().numOfAffected, pop.dvars().numOfUnaffected)
(500, 500)

Download DiscardIf.py