Different effect sizes of mutations in different demes#
New in version 0.7.0.
This section describes how to allow mutations to have different effect sizes in different demes.
To allow for correlated effect sizes across demes, we used fwdpy11.mvDES
to model multivariate
distributions of effect sizes. This class inherits from fwdpy11.Sregion
(see Distributions of effect sizes).
In general, there is no standard general method for drawing random deviates from arbitrary multivariate
distributions. The approach that we take is to use a multivariate Gaussian distribution as the underlying kernel.
First, we will describe the cases where the multivariate Gaussian kernel leads to output effect size distributions
that have straightforward interpretations. Then, we will move onto the more general case allowing you to construct
multivariate distributions from current fwdpy11.Sregion
types.
The multivariate Gaussian#
We may model Gaussian effect sizes using the existing fwdpy11.MultivariateGaussianEffects
in conjunction with fwdpy11.mvDES
. Using fwdpy11.MultivariateGaussianEffects
on its
own is used to model pleiotropic effects on a trait. Here, we are re-using this type to model correlated
effect sizes across demes.
At this time, it is probably best to look at an example. The following code models Gaussian stabilizing selection on a quantitative trait. The effects sizes within each deme are themselves given by Gaussian distributions and there is no correlation in the effect size in the two demes.
import demes
import fwdpy11
import numpy as np
yaml = """
description: two demes with symmetric migration
time_units: generations
demes:
- name: deme0
epochs:
- start_size: 100
- name: deme1
epochs:
- start_size: 100
migrations:
- demes: [deme0, deme1]
rate: 0.1
"""
graph = demes.loads(yaml)
demography = fwdpy11.ForwardDemesGraph.from_demes(graph, 1)
optima = [fwdpy11.Optimum(when=0, optimum=0.0, VS=10.0)]
pdict = {
"nregions": [],
"recregions": [],
"sregions": [
fwdpy11.mvDES(
fwdpy11.MultivariateGaussianEffects(0, 1, 1, np.identity(2)), np.zeros(2)
)
],
"rates": (0, 1e-3, None),
"demography": demography,
"simlen": 100,
"gvalue": fwdpy11.Additive(
ndemes=2, scaling=2,
gvalue_to_fitness=fwdpy11.GaussianStabilizingSelection.single_trait(optima)
),
"prune_selected": False,
}
Most of the above is standard. Let’s dissect the new bits:
An instance of
fwdpy11.mvDES
is our only region with selected mutations.This instance holds an instance of
fwdpy11.MultivariateGaussianEffects
that puts mutations on the interval \([0, 1)\) with weight 1 and an identity matrix specifies the correlation in effect sizes between demes 0 and 1. The identity matrix has the value zero for all off-diagonal elements, meaning no covariance in effect sizes across demes.The final constructor argument specifies the mean of each marginal Gaussian distribution. The means are both zero.
Our genetic value type accepts an
ndemes
parameter, telling it that it has to look for deme-specific effect sizes. This value must be set to the maximum number of demes that will exist during a simulation.
Let’s evolve the model now:
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
rng = fwdpy11.GSLrng(1010)
fwdpy11.evolvets(rng, pop, params, 10)
Let’s extract the effect sizes from each deme:
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[ 0.09742853 -0.79068629]
[-0.46314495 -0.31686816]
Let’s look at another example where effect sizes covary negatively across demes and raise the mutation rate a bit:
vcv = np.array([1.0, -0.99, -0.99, 1.0]).reshape((2, 2))
pdict["sregions"] = [
fwdpy11.mvDES(fwdpy11.MultivariateGaussianEffects(0, 1, 1, vcv), np.zeros(2))
]
pdict["rates"] = (0, 5e-3, None)
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.65688902 0.67458945]
[ 1.44236014 -1.19184102]
[ 0.53515552 -0.49690106]
[0.11239562 0.03076654]
[ 0.0470034 -0.14227415]
[-0.44707712 0.39814511]
[-0.64860038 0.53073426]
[-0.12680578 -0.00521323]
[-1.702493 1.78230999]
[-0.12798166 -0.14205828]
[ 0.82686083 -0.68915806]
Now we see that the effect sizes often differ in sign between the two demes.
The multivariate lognormal#
If \(X\) is a multivariate Gaussian distribution, \(N(\mathbf{\mu}, \mathbf{\sum})\), where \(\mathbf{\mu}\) is a vector of mean values and \(\mathbf{\sum}\) is the covariance matrix, then \(Y = e^X\) is a multivariate lognormal random variable with mean \(E[Y]_i = e^{\mu_i + \frac{1}{2}\sum_{ii}}\) and covariance matrix \(Var[Y]_{i,j} = e^{\mu_i + \mu_j + \frac{1}{2}(\sum_{ii} + \sum_{jj})}(e^{\sum_{ij}}-1)\).
To specify a multivariate lognormal distribution of effect sizes, we use
the static class method fwdpy11.LogNormalS.mv()
. The following code
constructs a distribution of effect sizes such that -2Ns
(where N
is the
size of a single deme) is a multivariate lognormal with means zero and an
identity matrix as a covariance matrix used to specify the multivate
Gaussian kernel.
mvdes = fwdpy11.mvDES(
fwdpy11.LogNormalS.mv(0, 1, 1, scaling=-200), np.zeros(2), np.identity(2)
)
Note
The lognormal distribution returns deviates \(> 0\).
To model deleterious mutations/effect sizes < 0, use the
scaling
parameter with a negative value like we just did!
Let’s put it in a simulation and run it:
pdict = {
"nregions": [],
"recregions": [],
"sregions": [mvdes],
"rates": (0, 1e-3, None),
"demography": demography,
"simlen": 100,
"gvalue": fwdpy11.Multiplicative(ndemes=2, scaling=2),
}
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.00183755 -0.0009758 ]
[-0.00110216 -0.00049425]
“Custom” multivariate distributions#
The previous two sections cover cases where the methods for generating deviates from a multivariate distribution are straightforward and agreed upon.
In order to simulate multivariate distributions of effect sizes based on
fwdpy11.Sregion
types, we follow a fairly intuitive approach
described in [XKS00]. Briefly, the multivariate Gaussian kernel is
used to produce deviates. Then, the quantiles from the cummulative distribution
of each marginal Gaussian are used to generate a deviate from the desired output distribution of interest.
For a simulation with n
populations we need:
A
list
ofn
fwdpy11.Sregion
objectsAn array of
n
means for the multivariate GaussianAn
n-by-n
covariance matrix for the multivariate Gaussian
The following generates exponentially distributed effect sizes in each deme with a high correlation across demes:
mvdes = fwdpy11.mvDES(
[fwdpy11.ExpS(0, 1, 1, -0.5)] * 2,
np.zeros(2),
np.matrix([1, 0.9, 0.9, 1]).reshape((2, 2)),
)
pdict["sregions"] = [mvdes]
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.05297616 -0.05994487]
[-0.00600096 -0.00063 ]
We can mix and match our distributions. Here, the distribution of effect sizes in deme 0 is exponential and the distribution in deme 1 is gamma. The two distributions have means with opposite signs and the magnitudes of the marginal deviates negatively covary:
mvdes = fwdpy11.mvDES(
[fwdpy11.ExpS(0, 1, 1, -0.5), fwdpy11.GammaS(0, 1, 1, mean=0.1, shape_parameter=1)],
np.zeros(2),
np.matrix([1, -0.9, -0.9, 1]).reshape((2, 2)),
)
pdict["sregions"] = [mvdes]
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.17037555 0.17324704]
[-0.22122016 0.22419496]
[-1.78012379 0.01713378]
[-0.36227095 0.10331541]
[-0.24379116 0.12145104]
The type fwdpy11.ConstantS
has intuitive behavior:
mvdes = fwdpy11.mvDES(
[fwdpy11.ExpS(0, 1, 1, -0.5), fwdpy11.ConstantS(0, 1, 1, -0.1)],
np.zeros(2),
np.matrix([1, -0.9, -0.9, 1]).reshape((2, 2)),
)
pdict["rates"] = (0, 5e-3, None)
pdict["sregions"] = [mvdes]
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
rng = fwdpy11.GSLrng(1010)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.99150137 -0.1 ]
[-0.22828726 -0.1 ]
[-0.10570464 -0.1 ]
[-0.06271967 -0.1 ]
Recipes#
Different signs in different demes#
Consider two demes. You want any beneficial mutation in one deme to be deleterious in the other and vice-versa.
For the multivariate Gaussian, use the covariance matrix as done above. Note that this approach only generates a tendency to different signs in different demes.
With the multivariate lognormal, the best we can do is to use negative correlations such that deleterious mutations in deme 0 are less deleterious in deme 1, etc.:
sregions = [
fwdpy11.mvDES(
fwdpy11.LogNormalS.mv(0, 1, 1, scaling=-200),
np.zeros(2),
np.matrix([1, -0.99, -0.99, 1]).reshape((2, 2)),
)
]
sregions.append(
fwdpy11.mvDES(
fwdpy11.LogNormalS.mv(0, 1, 1, scaling=200),
np.zeros(2),
np.matrix([1, -0.99, -0.99, 1]).reshape((2, 2)),
)
)
pdict["sregions"] = sregions
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
rng = fwdpy11.GSLrng(1010)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[0.00039308 0.0693669 ]
[-0.01488498 -0.00173671]
[-0.00514551 -0.00519719]
[-0.00377736 -0.00592563]
[0.00069116 0.0453049 ]
[0.00355528 0.00643812]
[-0.00208255 -0.01409883]
[-0.01590584 -0.00175813]
[-0.00045263 -0.04933448]
[0.00227164 0.01008629]
[0.01128436 0.00198865]
[0.01109052 0.00222656]
[-0.00256051 -0.01002711]
[-0.00152782 -0.01807099]
In the output, we see that an effect size in deme i
has a corresponding effect size in deme j
that is a about an order of magnitude smaller in absolute value.
For the general approach, simply create a list
of objects with the desired mean (or constant) effect sizes. For example:
sregions = [
fwdpy11.mvDES(
[fwdpy11.ExpS(0, 1, 1, -0.5), fwdpy11.ExpS(0, 1, 1, 0.1)],
np.zeros(2),
np.identity(2),
)
]
sregions.append(
fwdpy11.mvDES(
[fwdpy11.ExpS(0, 1, 1, 0.1), fwdpy11.ExpS(0, 1, 1, -0.5)],
np.zeros(2),
np.identity(2),
)
)
pdict["sregions"] = sregions
params = fwdpy11.ModelParams(**pdict)
pop = fwdpy11.DiploidPopulation([100, 100], 1.0)
rng = fwdpy11.GSLrng(1010)
fwdpy11.evolvets(rng, pop, params, 10)
for i in pop.tables.mutations:
print(pop.mutations[i.key].esizes)
[-0.99150137 0.08290456]
[ 0.04565745 -0.16002424]
[-0.10570464 0.21656547]
[ 0.41101789 -0.43686025]
[-0.88406416 0.16572598]
[ 0.25571088 -0.08508201]
[ 0.15472792 -0.292425 ]
[-0.14495801 0.08997944]
[-0.06271967 0.15351897]