Hello,
following a chat to Ben, I'm writing here a bit about the continuum of alleles (CA) models, and opening the space for ideas of optimization in terms of implementation, the possibility of native support if it could be further improved under the hood (say a new stackPolicy 'a' for accumulate or 'ar' accumulate and replace).
The continuum of alleles (CA) model in population genetics describes a scenario in which a quantitative trait is influenced by a very large number of alleles, each contributing with an extremely small (ideally infinitesimal) effect to the quantitative trait. In CA models, this set of many alleles is assumed to be inherited together without recombination among them, meaning that their individual identities are less important than their cumulative effect. Because many alleles are thought to underlie this trait, the mutation rate in CA models is typically quite large (say 0.1 or 0.01), and this model is often summarized as "a trait evolving by many mutations of small effect".
This model is particularly useful when the interest is the distribution of a trait in a population (like its mean, variance, or possible polymorphisms) rather than the specific genetic architecture (i.e., which genes or loci are involved; although there are studies of genetic architecture that use CA models or modifications of these models). Furthermore, it allows for simplifying assumptions that make theoretical modeling and prediction more tractable, so people using SLiM to verify and expand mathematical results might also be very interested in efficient implementations of CA models in SLiM.
At this point, you're probably thinking that the CA model would be equivalent to our (default)StackPolicy as "stack", where within a linkage group there can be many non-recombining mutations whose overall effect is the sum of the effect of each mutation. And indeed it is. But I don't think that this implementation is the most efficient way to implement the CA model on SLiM, both in terms of time as well as memory.
Instead, because we are not interested in the individual identities of each mutation (and because mutation rate is high there will be many many many of these), I find that the most efficient way is to have a single "representative allele" at each position. In practice, this means that the StackPolicy is changed to "last", and whenever any new mutation appear at a position that already contains a representative allele, it's selectionCoeff accumulates the previous effect together with its own new effect. The mutation callback looks like this
mutation(m1) /* Modeling continuum of alleles: a mix between stack (accumulating selCoeff) and last (having a single mutation per position) policies */
{
// Get position of new mutation
mutpos = mut.position;
// Gets which previous mutation was in that position
allMuts = haplosome.mutationsOfType(m1);
parentalMut = allMuts[allMuts.position == mutpos];
// If there was no previous mutation, just accept
if (size(parentalMut) == 0) return T;
// If there was a previous mutation, accumulate selection coefficients
prev = parentalMut.selectionCoeff;
mut.setSelectionCoeff(prev + mut.selectionCoeff);
// Always accept
return T;
}
Testing on my PC the code I'm attaching to the end of this message (a neutral simulation of a quantitative trait affected by 50 unlinked CA loci), I find a 7% reduction in running time (here measured between tick 1'000 and 5'000, with mutation rate 10^-3):
- Using last policy + accumulating selectionCoeff: 216.0s
- Using stack policy: 233.4s
and I imagine as the number of mutations per position increase in the "stack policy" version of the code, the benefit of this implementation can also increases. I also imagine there is a saving in terms of memory because there are less mutation objects coexisting within the simulation.
Also, I think converting mutations to substitutions here wouldn't save us from having many mutations segregating at a given time because as mutation rate is high and there is full linkage between mutations, things like compensation and redundancy among mutations would decrease the chances that a mutation is present across all individuals (I imagine it would be similar to the neutral probabilities of fixation in fact, but it probably depends on selective forces. Under disruptive selection, where multiple morphs can be maintained, mutations are in fact likely to never fix).
I'd be happy to hear any thoughts on how to improve it, and if you think this could be a native supported Stack Policy (especially if you think this could be further accelerated under the hood).
@bhaller , you mentioned this could be a recipe in the manual -- let's see if we gather more feedback on this Issue first and I'll compile some paragraphs similar to those written here describing the CA model and an example for you.
// set up a simple quantitative trait simulation
initialize() {
setSeed(0);
initializeMutationRate(1e-3);
initializeMutationType("m1", 0.5, "n", 0.0, 0.01);
initializeGenomicElementType("g1", m1, 1.0);
m1.mutationStackPolicy = 'l'; //comment this line and the mutation callback for the stacking version
m1.convertToSubstitution = F;
initializeGenomicElement(g1, 0, 49);
initializeRecombinationRate(0.5);
}
// create a population of 500 individuals
1 early() { sim.addSubpop("p1", 500); }
// calculates the phenotype at every generation (despite not using it, just to test)
1:5000 late() { p1.individuals.tagF = p1.individuals.sumOfMutationsOfType(m1); }
// turning off the QTL default fitness effect
mutationEffect(m1) { return 1.0; }
mutation(m1) /* Modeling continiuum of alleles: a mix between stack and last */
{
// Get position of new mutation
mutpos = mut.position;
// Gets which previous mutation was in that position
allMuts = haplosome.mutationsOfType(m1);
parentalMut = allMuts[allMuts.position == mutpos];
// If there was no previous mutation, just accept
if (size(parentalMut) == 0) return T;
// If there was a previous mutation, accumulate selection coefficients
prev = parentalMut.selectionCoeff;
mut.setSelectionCoeff(prev + mut.selectionCoeff);
// Always accept
return T;
}
Hello,
following a chat to Ben, I'm writing here a bit about the continuum of alleles (CA) models, and opening the space for ideas of optimization in terms of implementation, the possibility of native support if it could be further improved under the hood (say a new stackPolicy 'a' for accumulate or 'ar' accumulate and replace).
The continuum of alleles (CA) model in population genetics describes a scenario in which a quantitative trait is influenced by a very large number of alleles, each contributing with an extremely small (ideally infinitesimal) effect to the quantitative trait. In CA models, this set of many alleles is assumed to be inherited together without recombination among them, meaning that their individual identities are less important than their cumulative effect. Because many alleles are thought to underlie this trait, the mutation rate in CA models is typically quite large (say 0.1 or 0.01), and this model is often summarized as "a trait evolving by many mutations of small effect".
This model is particularly useful when the interest is the distribution of a trait in a population (like its mean, variance, or possible polymorphisms) rather than the specific genetic architecture (i.e., which genes or loci are involved; although there are studies of genetic architecture that use CA models or modifications of these models). Furthermore, it allows for simplifying assumptions that make theoretical modeling and prediction more tractable, so people using SLiM to verify and expand mathematical results might also be very interested in efficient implementations of CA models in SLiM.
At this point, you're probably thinking that the CA model would be equivalent to our (default)
StackPolicyas "stack", where within a linkage group there can be many non-recombining mutations whose overall effect is the sum of the effect of each mutation. And indeed it is. But I don't think that this implementation is the most efficient way to implement the CA model on SLiM, both in terms of time as well as memory.Instead, because we are not interested in the individual identities of each mutation (and because mutation rate is high there will be many many many of these), I find that the most efficient way is to have a single "representative allele" at each position. In practice, this means that the
StackPolicyis changed to "last", and whenever any new mutation appear at a position that already contains a representative allele, it'sselectionCoeffaccumulates the previous effect together with its own new effect. The mutation callback looks like thisTesting on my PC the code I'm attaching to the end of this message (a neutral simulation of a quantitative trait affected by 50 unlinked CA loci), I find a 7% reduction in running time (here measured between tick 1'000 and 5'000, with mutation rate 10^-3):
and I imagine as the number of mutations per position increase in the "stack policy" version of the code, the benefit of this implementation can also increases. I also imagine there is a saving in terms of memory because there are less mutation objects coexisting within the simulation.
Also, I think converting mutations to substitutions here wouldn't save us from having many mutations segregating at a given time because as mutation rate is high and there is full linkage between mutations, things like compensation and redundancy among mutations would decrease the chances that a mutation is present across all individuals (I imagine it would be similar to the neutral probabilities of fixation in fact, but it probably depends on selective forces. Under disruptive selection, where multiple morphs can be maintained, mutations are in fact likely to never fix).
I'd be happy to hear any thoughts on how to improve it, and if you think this could be a native supported Stack Policy (especially if you think this could be further accelerated under the hood).
@bhaller , you mentioned this could be a recipe in the manual -- let's see if we gather more feedback on this Issue first and I'll compile some paragraphs similar to those written here describing the CA model and an example for you.