Demographic changes¶
A mating scheme controls the size of an offspring generation using parameter
subPopSize
. This parameter has been described in detail in section
subsec_offspring_size. In summary,
- The subpopulation sizes of the offspring generation will be the same as the parental generation if subPopSize is not set.
- The offspring generation will have a fixed size if
subPopSize
is set to a number (no subpopulation) or a list of subpopulation sizes. - The subpopulation sizes of an offspring generation will be determined by the
return value of a demographic function if
subPopSize
is set to such a function (a function that returns subpopulation sizes at each generation).
Note
Parameter subPopSize
only controls subpopulation sizes of an offspring
generation immediately after it is generated. population or subpopulation sizes
could be changed by other operators.
During mating, a mating scheme goes through each parental subpopulation and populates its corresponding offspring subpopulation. This implies that
- Parental and offspring populations should have the same number of subpopulations.
- Mating happens strictly within each subpopulation.
This section will introduce several operators that allow you to move dndividuals
across the boundary of subpopulations (migration), and change the number of
subpopulations during evolution (split and merge). Please refer to
subsec_offspring_size (control the size of the
offspring generation section of chapter mating scheme) for more details. For
more advanced demographic models, please refer to the simuPOP.demography
module.
Migration (operator Migrator
)¶
Migration by probability¶
Operator Migrator
(and its function form migrate
) migrates
individuals from one subpopulation to another. The key parameters are
- from subpopulations (parameter
subPops
). A list of subpopulations from which individuals migrate. Default to all subpopulations. - to subpopulations (parameter
toSubPops
). A list of subpopulations to which individuals migrate. Default to all subpopulations. A new subpopulation ID can be specified to create a new subpopulation from migrants. - A migration rate matrix (parameter
rate
). A \(m\) by \(n\) matrix ( a nested list in Python) that specifies migration rate from each source to each destination subpopulation. That is to say, \(\mbox{rate}{}_{i,j}\) specifies migration rate from \(\mbox{subPops}_{i}\) to \(\mbox{toSubPops}_{j}\). Needless to say, \(m\) and \(n\) are determined by the number of from and to subpopulations.
Example migrateByProb demonstrate the use of a
Migrator
to migrate individuals between three subpopulations. Note that
- Operator
Migrator
relies on an information fieldmigrate_to
(configurable) to record destination subpopulation of each individual so this information field needs to be added to a population befor migration. - Migration rates to subpopulation themselves are determined automatically so they can be left unspecified.
Example: Migration by probability
>>> import simuPOP as sim
>>> pop = sim.Population(size=[1000]*3, infoFields='migrate_to')
>>> pop.evolve(
... initOps=sim.InitSex(),
... preOps=sim.Migrator(rate=[
... [0, 0.1, 0.1],
... [0, 0, 0.1],
... [0, 0.1, 0]
... ]),
... matingScheme=sim.RandomMating(),
... postOps=[
... sim.Stat(popSize=True),
... sim.PyEval('subPopSize'),
... sim.PyOutput('\n')
... ],
... gen = 5
... )
[762, 1108, 1130]
[601, 1175, 1224]
[490, 1233, 1277]
[395, 1282, 1323]
[320, 1300, 1380]
5
now exiting runScriptInteractively...
Migration by proportion and counts¶
Migration rate specified in the rate parameter in Example migrateByProb is intepreted as probabilities. That is to say, a migration rate \(r_{m,n}\) is interpreted as the probability at which any individual in subpopulation \(m\) migrates to subpopulation \(n\). The exact number of migrants are randomly distributed.
If you would like to specify exactly how many migrants migrate from a
subpopulation to another, you can specify parameter mode
of operator
Migrator
to BY_PROPORTION
or BY_COUNTS
. The BY_PROPORTION
mode interpret \(r_{m,n}\) as proportion of individuals who will migrate
from subpopulation \(m\) to \(n\) so the number of \(m\rightarrow
n\) migrant will be exactly \(r_{m,n}\times\)subPopSize(m). In the
BY_COUNTS
mode, \(r_{m,n}\) is interpretted as number of migrants,
regardless the size of subpopulation \(m\). Example
migrateByPropAndCount demonstrates these two
migration modes, as well as the use of parameters subPops
and toSubPops.
Example: Migration by proportion and count
>>> import simuPOP as sim
>>> pop = sim.Population(size=[1000]*3, infoFields='migrate_to')
>>> pop.evolve(
... initOps=sim.InitSex(),
... preOps=sim.Migrator(rate=[[0.1], [0.2]],
... mode=sim.BY_PROPORTION,
... subPops=[1, 2],
... toSubPops=[3]),
... matingScheme=sim.RandomMating(),
... postOps=[
... sim.Stat(popSize=True),
... sim.PyEval('subPopSize'),
... sim.PyOutput('\n')
... ],
... gen = 5
... )
[1000, 900, 800, 300]
[1000, 810, 640, 550]
[1000, 729, 512, 759]
[1000, 657, 410, 933]
[1000, 592, 328, 1080]
5
>>> #
>>> pop.evolve(
... preOps=sim.Migrator(rate=[[50, 50], [100, 50]],
... mode=sim.BY_COUNTS,
... subPops=[3, 2],
... toSubPops=[2, 1]),
... matingScheme=sim.RandomMating(),
... postOps=[
... sim.Stat(popSize=True),
... sim.PyEval('subPopSize'),
... sim.PyOutput('\n')
... ],
... gen = 5
... )
[1000, 692, 328, 980]
[1000, 792, 328, 880]
[1000, 892, 328, 780]
[1000, 992, 328, 680]
[1000, 1092, 328, 580]
5
now exiting runScriptInteractively...
Theoretical migration models¶
To facilitate the use of widely used theoretical migration models, a few
functions are defined in module simuPOP.demography
subsec_Predefined_migration_models.
These functions generate migration matrixes that can be plugged in to the
Migrator
operator.
migrate from virtual subpopulations *¶
Under a realistic eco-social settings, individuals in a subpopulation rarely have the same probability to migrate. Genetic evidence has shown that female has a higher migrate rate than male in humans, perhaps due to migration patterns related to inter-population marriages. Such sex-biased migration also happens in other large migration events such as slave trade.
It is easy to simulate most of such complex migration models by migrating from
virtual subpopulations. For example, if you define virtual subpopulations by
sex, you can specify different migration rates for males and females and control
the proportion of males among migrants, by specifying virtual subpopulations in
parameter subPops
. Parameter toSubPops
does not accept virtual
subpopulations because you cannot, for example, migrate to females in a
subpopulation.
Example migrateVSP demonstrate a sex-biased migration model where males dominate migrants from subpopulation 0. To avoid confusing, this example uses the proportion migration mode. At the beginning of the first generation, there are 500 males and 500 females in each subpopulation. A 10% male migration rate and 5% female migration rate leads to 50 male migrants and 25 female migrants. Subpopulation sizes and number of males in each subpopulation before mating are therefore:
- Subpopulation 0: male 500-50, female 500-25, total 925
- Subpopulation 1: male 500+50, female 500+25, total 1075
Note that the unspecified to subpopulations are subpopulation 0 and 1, which cannot be virtual.
Example: Migration from virtual subpopulations
>>> import simuPOP as sim
>>> pop = sim.Population(size=[1000]*2, infoFields='migrate_to')
>>> pop.setVirtualSplitter(sim.SexSplitter())
>>> pop.evolve(
... # 500 males and 500 females
... initOps=sim.InitSex(sex=[sim.MALE, sim.FEMALE]),
... preOps=[
... sim.Migrator(rate=[
... [0, 0.10],
... [0, 0.05],
... ],
... mode = sim.BY_PROPORTION,
... subPops=[(0, 0), (0, 1)]),
... sim.Stat(popSize=True, numOfMales=True, vars='numOfMales_sp'),
... sim.PyEval(r"'%d/%d\t%d/%d\n' % (subPop[0]['numOfMales'], subPopSize[0], "
... "subPop[1]['numOfMales'], subPopSize[1])"),
... ],
... matingScheme=sim.RandomMating(),
... postOps=[
... sim.Stat(popSize=True, numOfMales=True, vars='numOfMales_sp'),
... sim.PyEval(r"'%d/%d\t%d/%d\n' % (subPop[0]['numOfMales'], subPopSize[0], "
... "subPop[1]['numOfMales'], subPopSize[1])"),
... ],
... gen = 2
... )
450/925 550/1075
426/925 520/1075
384/859 562/1141
425/859 582/1141
2
now exiting runScriptInteractively...
Arbitrary migration models **¶
If none of the described migration mothods fits your need, you can always resort to manual migration. One such example is when you need to mimick an existing evolutionary scenario so you know exactly which subpopulation each individual will migrate to.
Manual migration is actually very easy. All you need to do is specifying the
destination subpopulation of all individuals in the from subpopulations
(parameter subPops
), using an information field (usually migrate_to
).
You can then call the Migrator
using mode=BY_IND_INFO
. Example
manualMigration shows how to manually move individuals
around. This example uses the function form of Migrator
. You usually
need to use a Python operator to set destination subpopulations if you would
like to manually migrate individuals during an evolutionary process.
Example: Manual migration
>>> import simuPOP as sim
>>> pop = sim.Population([10]*2, infoFields='migrate_to')
>>> pop.setIndInfo([0, 1, 2, 3]*5, 'migrate_to')
>>> sim.migrate(pop, mode=sim.BY_IND_INFO)
>>> pop.subPopSizes()
(5, 5, 5, 5)
now exiting runScriptInteractively...
Note
individuals with an invalid destination subpopulation ID (e.g. an negative number) will be discarded silently. Although not recommended, this feature can be used to remove individuals from a subpopulation.
Migration using backward migration matrix (operator BackwardMigrator
)¶
Backward migration matrices are widely used in theoretical population genetics and coalescent based simulations. Instead of specifying the probability of migrating from one subpopulation to another (namely how migration happens), such matrices specify the probability that individuals in a subpopulation originate from others (namely the result of migration). simuPOP simulates such models by converting backward migration matrices to foward ones using the theory described below. Due to the limit of such models, simuPOP cannot simulate migration from/to virtual subpopulatons, creation of new subpopulation, different source and destination subpopulations, and will generate an error if the conversion process fails.
To explain the differences between forward and backward migration matrices, let us assume that there are \(d\) subpopulations with population sizes \(S=\left[S_{1},S_{2},...,S_{d}\right]\), and a forward migration matrix
where \(f_{ij}\) is the probability that an individual will migration from subpopulation \(i\) to \(j\). After migration happens, subppulation sizes are changed to \(S'=\left[S'_{1},S'_{2},...,S'_{d}\right]\), and the origin of individuals in each subpopulation can be described by the backward migration matrix
where \(b_{ij}\) is the probability that an individual in subpopulation \(i\) originates from subpopulation \(j\).
These qualities can be derived from original population sizes and the forward migration matrix. That is to say, the size of new subpopulation \(k\) is the sum of all migrants to this subpopulation
and the size of the original population \(k\) is the sum of all migrants from this subpopulation
and the composition of subpopulation \(k\) (e.g. individuals originate from subpopulation \(j\)) is
In matrix form, these formulas can be written as
and
Therefore, given a backward migration matrix \(B\) and current population size \(S\), we can derive a forward migration matrix using
and
Note that \(F=B\) is always true if \(B\) is symmetric and \(S_{i}=S_{j}\) (equal subpopulation size) so simuPOP will use \(B\) directly in this case. Also note that \(B\) might not be inversable and \(S'\) and \(F\) might be invalid (e.g. negative population size or forward migration rate) for given \(B\) and \(S\). simuPOP will terminate with an error message in these cases.
The following example backwardMigration demonstrates how to use a backward migration matrix to perform migration. It initializes all individuals with indexes of subpopulations they belong to before migration and calculates the percent of individuals from each source population using a PyOperator with function originOfInds. The so-called overseved backward migration matrix is similar to specified migration matrix despite of stochastic effects. This example also uses turnOnDebug function to let the operator print the expected subpopulation size (\(S'\)) and calculate forward migration matrix (\(F\)) at each generation, which, as expected, vary from generation to generation.
Example: Migration using a backward migration matrix
>>> import simuPOP as sim
>>> sim.turnOnDebug('DBG_MIGRATOR')
>>> pop = sim.Population(size=[10000, 5000, 8000], infoFields=['migrate_to', 'migrate_from'])
>>> def originOfInds(pop):
... print('Observed backward migration matrix at generation {}'.format(pop.dvars().gen))
... for sp in range(pop.numSubPop()):
... # get source subpop for all individuals in subpopulation i
... origins = pop.indInfo('migrate_from', sp)
... spSize = pop.subPopSize(sp)
... B_sp = [origins.count(j) * 1.0 /spSize for j in range(pop.numSubPop())]
... print(' ' + ', '.join(['{:.3f}'.format(x) for x in B_sp]))
... return True
...
>>> pop.evolve(
... initOps=sim.InitSex(),
... preOps=
... # mark the source subpopulation of each individual
... [sim.InitInfo(i, subPops=i, infoFields='migrate_from') for i in range(3)] + [
... # perform migration
... sim.BackwardMigrator(rate=[
... [0, 0.04, 0.02],
... [0.05, 0, 0.02],
... [0.02, 0.01, 0]
... ]),
... # calculate and print observed backward migration matrix
... sim.PyOperator(func=originOfInds),
... # calculate population size
... sim.Stat(popSize=True),
... # and print it
... sim.PyEval(r'"Pop size after migration: {}\n".format(", ".join([str(x) for x in subPopSize]))'),
... ],
... matingScheme=sim.RandomMating(),
... gen = 5
... )
Expected next population size is 10211.4, 4851.8, 7936.84
Forward migration matrix is 0.959867, 0.024259, 0.0158737, 0.0816908, 0.902435, 0.0158737, 0.0255284, 0.0121295, 0.962342
Observed backward migration matrix at generation 0
0.939, 0.040, 0.021
0.051, 0.927, 0.022
0.020, 0.010, 0.969
Pop size after migration: 10218, 4859, 7923
Expected next population size is 10453.6, 4690.64, 7855.79
Forward migration matrix is 0.961671, 0.0229529, 0.0153764, 0.0860553, 0.897777, 0.0161675, 0.0263879, 0.0118406, 0.961772
Observed backward migration matrix at generation 1
0.942, 0.038, 0.020
0.049, 0.932, 0.020
0.023, 0.010, 0.968
Pop size after migration: 10417, 4706, 7877
Expected next population size is 10675.5, 4517.1, 7807.37
Forward migration matrix is 0.963329, 0.0216814, 0.0149897, 0.0907397, 0.89267, 0.0165902, 0.0271056, 0.0114691, 0.961425
Observed backward migration matrix at generation 2
0.942, 0.039, 0.020
0.048, 0.930, 0.022
0.020, 0.010, 0.970
Pop size after migration: 10660, 4536, 7804
Expected next population size is 10946, 4323.5, 7730.53
Forward migration matrix is 0.965217, 0.0202791, 0.0145038, 0.0965253, 0.886432, 0.0170426, 0.0280522, 0.0110802, 0.960868
Observed backward migration matrix at generation 3
0.940, 0.040, 0.020
0.050, 0.930, 0.020
0.020, 0.011, 0.969
Pop size after migration: 10942, 4321, 7737
Expected next population size is 11260.4, 4079.55, 7660
Forward migration matrix is 0.967357, 0.0186417, 0.0140011, 0.104239, 0.878033, 0.0177274, 0.0291081, 0.0105456, 0.960346
Observed backward migration matrix at generation 4
0.937, 0.043, 0.021
0.046, 0.933, 0.021
0.019, 0.009, 0.972
Pop size after migration: 11331, 4042, 7627
5
now exiting runScriptInteractively...
Split subpopulations (operators SplitSubPops
)¶
Operator SplitSubPops
splits one or more subpopulations into finer
subpopulations. It can be used to simulate populations that originate from the
same founder population. For example, a population of size 1000 in Example
splitBySize is split into three subpopulations of sizes
300, 300 and 400 respectively, after evolving as a single population for two
generations.
Example: Split subpopulations by size
>>> import simuPOP as sim
>>> pop = sim.Population(1000)
>>> pop.evolve(
... preOps=[
... sim.SplitSubPops(subPops=0, sizes=[300, 300, 400], at=2),
... sim.Stat(popSize=True),
... sim.PyEval(r'"Gen %d:\t%s\n" % (gen, subPopSize)')
... ],
... matingScheme=sim.RandomSelection(),
... gen = 4
... )
Gen 0: [1000]
Gen 1: [1000]
Gen 2: [300, 300, 400]
Gen 3: [300, 300, 400]
4
now exiting runScriptInteractively...
Operator SplitSubPops
splits a subpopulation by sizes of the resulting
subpopulations. It is often easier to do so with proportions. In addition, if a
demographic function is used, you should make sure that the number of
subpopulations will be the same before and after mating at any generation. One
way of doing this is to apply a SplitSubPops
operator at the right
generation. Example splitByProp demonstrates such an
evolutionary scenario. However, it is often easier to split the population in
the demographic function in such case (see section
subsec_Advanced_demo_func for details).
Example: Split subpopulations by proportion
>>> import simuPOP as sim
>>> def demo(gen, pop):
... if gen < 2:
... return 1000 + 100 * gen
... else:
... return [x + 50 * gen for x in pop.subPopSizes()]
...
>>> pop = sim.Population(1000)
>>> pop.evolve(
... preOps=[
... sim.SplitSubPops(subPops=0, proportions=[.5]*2, at=2),
... sim.Stat(popSize=True),
... sim.PyEval(r'"Gen %d:\t%s\n" % (gen, subPopSize)')
... ],
... matingScheme=sim.RandomSelection(subPopSize=demo),
... gen = 4
... )
Gen 0: [1000]
Gen 1: [1000]
Gen 2: [550, 550]
Gen 3: [650, 650]
4
now exiting runScriptInteractively...
Either by sizes or by proportions, individuals in a subpopulation are
divided randomly. It is, however, also possible to split subpopulations
according to individual information fields. In this case, individuals with
different values at a given information field will be split into different
subpopulations. This is demonstrated in Example splitByInfo
where the function form of operator SplitSubPops
is used.
Example: Split subpopulations by individual information field
>>> import simuPOP as sim
>>> import random
>>> pop = sim.Population([1000]*3, subPopNames=['a', 'b', 'c'], infoFields='x')
>>> pop.setIndInfo([random.randint(0, 3) for x in range(1000)], 'x')
>>> print(pop.subPopSizes())
(1000, 1000, 1000)
>>> print(pop.subPopNames())
('a', 'b', 'c')
>>> sim.splitSubPops(pop, subPops=[0, 2], infoFields=['x'])
>>> print(pop.subPopSizes())
(243, 244, 262, 251, 1000, 243, 244, 262, 251)
>>> print(pop.subPopNames())
('a', 'a', 'a', 'a', 'b', 'c', 'c', 'c', 'c')
now exiting runScriptInteractively...
Merge subpopulations (operator MergeSubPops
)¶
Operator MergeSubPops
merges specified subpopulations into a single
subpopulation. This operator can be used to simulate admixed populations where
two or more subpopulations merged into one subpopulation and continue to evolve
for a few generations. Example MergeSubPops simulates such
an evolutionary scenario. A demographic model could be added similar to Example
splitByProp.
Example: Merge multiple subpopulations into a single subpopulation
>>> import simuPOP as sim
>>> pop = sim.Population([500]*2)
>>> pop.evolve(
... preOps=[
... sim.MergeSubPops(subPops=[0, 1], at=3),
... sim.Stat(popSize=True),
... sim.PyEval(r'"Gen %d:\t%s\n" % (gen, subPopSize)')
... ],
... matingScheme=sim.RandomSelection(),
... gen = 5
... )
Gen 0: [500, 500]
Gen 1: [500, 500]
Gen 2: [500, 500]
Gen 3: [1000]
Gen 4: [1000]
5
now exiting runScriptInteractively...
Resize subpopulations (operator ResizeSubPops
)¶
Whenever possible, it is recommended that subpopulation sizes are changed naturally, namely through the population of an offspring generation. However, it is sometimes desired to change the size of a population forcefully. Examples of such applications include immediate expansion of a small population before evolution, and the simulation of sudden population size change caused by natural disaster. By default, new individuals created by such sudden population expansion get their genotype from existing individuals. Example ResizeSubPops shows a scenario where two subpopulations expand instantly at generation 3.
Example: Resize subpopulation sizes
>>> import simuPOP as sim
>>> pop = sim.Population([500]*2)
>>> pop.evolve(
... preOps=[
... sim.ResizeSubPops(proportions=(1.5, 2), at=3),
... sim.Stat(popSize=True),
... sim.PyEval(r'"Gen %d:\t%s\n" % (gen, subPopSize)')
... ],
... matingScheme=sim.RandomSelection(),
... gen = 5
... )
Gen 0: [500, 500]
Gen 1: [500, 500]
Gen 2: [500, 500]
Gen 3: [750, 1000]
Gen 4: [750, 1000]
5
now exiting runScriptInteractively...
Time-dependent migration rate¶
In evolutionary scenarios with complex demographic models, number of subpopulations and migration rate might change from generation to generation. For example, if one of the subpopulations is split into two, the migration matrix has to be changed to accommendate increased number of subpopulations.
If there are a limited number of demographic changes and a few number of pre-
determined migration matrices. You can use a number of Migrators
that are
applied at different generations. For example, you can use the following
operators to apply the first migration scheme during first ten generations (0,
…, 9), and the second migration scheme during the rest of the evolutionary
process:
preOps=[
Migrator(rate=M1, end=9),
Migrator(rate=M2, begin=10),
]
If changes of demographies are frequent or stochastic so that migration matrices
can only be determined programmatically, it is easier to use a
PyOperator
to migrate populations using the function form of a
Migrator
. This is demonstrated in Example varyingMigr where migration matrixes are computed dynamically due to random
split of subpopulations.
Example: Varying migration rate
>>> import simuPOP as sim
>>>
>>> from simuPOP.utils import migrIslandRates
>>> import random
>>>
>>> def demo(pop):
... # this function randomly split populations
... numSP = pop.numSubPop()
... if random.random() > 0.3:
... pop.splitSubPop(random.randint(0, numSP-1), [0.5, 0.5])
... return pop.subPopSizes()
...
>>> def migr(pop):
... numSP = pop.numSubPop()
... sim.migrate(pop, migrIslandRates(0.01, numSP))
... return True
...
>>> pop = sim.Population(10000, infoFields='migrate_to')
>>> pop.evolve(
... initOps=sim.InitSex(),
... preOps=[
... sim.PyOperator(func=migr),
... sim.Stat(popSize=True),
... sim.PyEval(r'"Gen %d:\t%s\n" % (gen, subPopSize)')
... ],
... matingScheme=sim.RandomMating(subPopSize=demo),
... gen = 5
... )
Gen 0: [10000]
Gen 1: [4982, 5018]
Gen 2: [2495, 2505, 5000]
Gen 3: [2509, 2517, 4974]
Gen 4: [2512, 2512, 4976]
5
now exiting runScriptInteractively...