Skip to content

Commit 24bd765

Browse files
committed
add some multitrait unit tests; fix a couple of bugs
1 parent 39005f6 commit 24bd765

13 files changed

Lines changed: 244 additions & 34 deletions

QtSLiM/help/SLiMHelpClasses.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,9 +496,9 @@
496496
<p class="p6">Note that this relatedness is simply pedigree-based relatedness, and does not necessarily correspond to genetic relatedness, because of the effects of factors like assortment and recombination.<span class="Apple-converted-space">  </span>If a metric of actual genetic relatedness is desired, tree-sequence recording can be used after simulation is complete, to compute the exact genetic relatedness between individuals based upon the complete ancestry tree (a topic which is beyond the scope of this manual).<span class="Apple-converted-space">  </span>Actual genetic relatedness cannot presently be calculated during a simulation run; the information is implicitly contained in the recorded tree-sequence tables, but calculating it is too computationally expensive to be reasonable.</p>
497497
<p class="p6">This method assumes that the grandparents (or the parents, if grandparental information is not available) are themselves unrelated and that they are not inbred; this assumption is necessary because we have no information about their parentage, since SLiM’s pedigree tracking information only goes back two generations.<span class="Apple-converted-space">  </span>Be aware that in a model where inbreeding or selfing occurs at all (including “incidental selfing”, where a hermaphroditic individual happens to choose itself as a mate), some level of “background relatedness” will be present and this assumption will be violated.<span class="Apple-converted-space">  </span>In such circumstances, <span class="s1">relatedness()</span> will therefore tend to underestimate the degree of relatedness between individuals, and the greater the degree of inbreeding, the greater the underestimation will be.<span class="Apple-converted-space">  </span>If inbreeding is allowed in a model – and particularly if it is common – the results of <span class="s1">relatedness()</span> should therefore not be taken as an estimate of <i>absolute</i> relatedness, but can still be useful as an estimate of <i>relative</i> relatedness (indicating that, say, A appears from the information available to be more closely related to B than it is to C).</p>
498498
<p class="p6">See also <span class="s1">sharedParentCount()</span> for a different metric of relatedness.</p>
499-
<p class="p5"> (void)setOffsetForTrait([Nio&lt;Trait&gt; trait = NULL], [Nif offset = NULL])</p>
499+
<p class="p5">+ (void)setOffsetForTrait([Nio&lt;Trait&gt; trait = NULL], [Nif offset = NULL])</p>
500500
<p class="p6">Sets the individual offset(s) for the trait(s) specified by <span class="s1">trait</span>.<span class="Apple-converted-space">  </span>The traits can be specified as <span class="s1">integer</span> indices of traits in the species, or directly as <span class="s1">Trait</span> objects; <span class="s1">NULL</span> represents all of the traits in the species, in the order in which they were defined.</p>
501-
<p class="p6">The parameter <span class="s1">offset</span> must follow one of four patterns.<span class="Apple-converted-space">  </span>In the first pattern, offset is <span class="s1">NULL</span>; this sets the offset for each of the specified traits to its default value (<span class="s1">0.0</span> for additive traits, <span class="s1">1.0</span> for multiplicative traits) in each target individual.<span class="Apple-converted-space">  </span>In the second pattern, <span class="s1">offset</span> is a singleton value; this sets the given offset for each of the specified traits in each target individual.<span class="Apple-converted-space">  </span>In the third pattern, <span class="s1">offset</span> is of length equal to the number of specified traits; this sets the offset for each of the specified traits to the corresponding offset value in each target individual.<span class="Apple-converted-space">  </span>In the fourth pattern, <span class="s1">offset</span> is of length equal to the number of specified traits times the number of target individuals; this uses <span class="s1">offset</span> to provide a different offset value for each trait in each individual, using consecutive values from <span class="s1">offset</span> to set the offset for each of the specified traits in one individual before moving to the next individual.</p>
501+
<p class="p6">The parameter <span class="s1">offset</span> must follow one of four patterns.<span class="Apple-converted-space">  </span>In the first pattern, offset is <span class="s1">NULL</span>; this draws the offset for each of the specified traits from each trait’s individual-offset distribution (defined by each trait’s <span class="s1">individualOffsetMean</span> and <span class="s1">individualOffsetSD</span> properties) in each target individual.<span class="Apple-converted-space">  </span>(Note that individual offsets are automatically drawn from these distributions when an individual is created; this re-draws new offset values.)<span class="Apple-converted-space">  </span>In the second pattern, <span class="s1">offset</span> is a singleton value; this sets the given offset for each of the specified traits in each target individual.<span class="Apple-converted-space">  </span>In the third pattern, <span class="s1">offset</span> is of length equal to the number of specified traits; this sets the offset for each of the specified traits to the corresponding offset value in each target individual.<span class="Apple-converted-space">  </span>In the fourth pattern, <span class="s1">offset</span> is of length equal to the number of specified traits times the number of target individuals; this uses <span class="s1">offset</span> to provide a different offset value for each trait in each individual, using consecutive values from <span class="s1">offset</span> to set the offset for each of the specified traits in one individual before moving to the next individual.</p>
502502
<p class="p5"><span class="s3">+ (void)setSpatialPosition(float position)</span></p>
503503
<p class="p6"><span class="s3">Sets the spatial position of the individual (as accessed through the </span><span class="s4">spatialPosition</span><span class="s3"> property).<span class="Apple-converted-space">  </span>The length of </span><span class="s4">position</span><span class="s3"> (the number of coordinates in the spatial position of an individual) depends upon the spatial dimensionality declared with </span><span class="s4">initializeSLiMOptions()</span><span class="s3">.<span class="Apple-converted-space">  </span>If the spatial dimensionality is zero (as it is by default), it is an error to call this method.<span class="Apple-converted-space">  </span>The elements of </span><span class="s4">position</span><span class="s3"> are set into the values of the </span><span class="s4">x</span><span class="s3">, </span><span class="s4">y</span><span class="s3">, and </span><span class="s4">z</span><span class="s3"> properties (if those properties are encompassed by the spatial dimensionality of the simulation).<span class="Apple-converted-space">  </span>In other words, if the declared dimensionality is </span><span class="s4">"xy"</span><span class="s3">, calling </span><span class="s4">individual.setSpatialPosition(c(1.0, 0.5))</span><span class="s3"> property is equivalent to </span><span class="s4">individual.x = 1.0; individual.y = 0.5</span><span class="s3">; </span><span class="s4">individual.z</span><span class="s3"> is not set (even if a third value is supplied in </span><span class="s4">position</span><span class="s3">) since it is not encompassed by the simulation’s dimensionality in this example.</span></p>
504504
<p class="p6"><span class="s3">Note that this is an Eidos class method, somewhat unusually, which allows it to work in a special way when called on a vector of individuals.<span class="Apple-converted-space">  </span>When the target vector of individuals is non-singleton, this method can do one of two things.<span class="Apple-converted-space">  </span>If </span><span class="s4">position</span><span class="s3"> contains just a single point (i.e., is equal in length to the spatial dimensionality of the model), the spatial position of all of the target individuals will be set to the given point.<span class="Apple-converted-space">  </span>Alternatively, if </span><span class="s4">position</span><span class="s3"> contains one point per target individual (i.e., is equal in length to the number of individuals multiplied by the spatial dimensionality of the model), the spatial position of each target individual will be set to the corresponding point from </span><span class="s4">position</span><span class="s3"> (where the point data is concatenated, not interleaved, just as it would be returned by accessing the </span><span class="s4">spatialPosition</span><span class="s3"> property on the vector of target individuals).<span class="Apple-converted-space">  </span>Calling this method with a </span><span class="s4">position</span><span class="s3"> vector of any other length is an error.</span></p>

SLiMgui/SLiMHelpClasses.rtf

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4171,7 +4171,7 @@ See also
41714171
\f4\fs20 for a different metric of relatedness.\
41724172
\pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0
41734173

4174-
\f3\fs18 \cf2 \'96\'a0(void)setOffsetForTrait([Nio<Trait>\'a0trait\'a0=\'a0NULL], [Nif offset = NULL])\
4174+
\f3\fs18 \cf2 +\'a0(void)setOffsetForTrait([Nio<Trait>\'a0trait\'a0=\'a0NULL], [Nif\'a0offset\'a0=\'a0NULL])\
41754175
\pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0
41764176

41774177
\f4\fs20 \cf2 Sets the individual offset(s) for the trait(s) specified by
@@ -4183,15 +4183,16 @@ See also
41834183
\f4\fs20 objects;
41844184
\f3\fs18 NULL
41854185
\f4\fs20 represents all of the traits in the species, in the order in which they were defined.\
4186-
The parameter
4186+
\pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0
4187+
\cf2 The parameter
41874188
\f3\fs18 offset
41884189
\f4\fs20 must follow one of four patterns. In the first pattern, offset is
41894190
\f3\fs18 NULL
4190-
\f4\fs20 ; this sets the offset for each of the specified traits to its default value (
4191-
\f3\fs18 0.0
4192-
\f4\fs20 for additive traits,
4193-
\f3\fs18 1.0
4194-
\f4\fs20 for multiplicative traits) in each target individual. In the second pattern,
4191+
\f4\fs20 ; this draws the offset for each of the specified traits from each trait\'92s individual-offset distribution (defined by each trait\'92s
4192+
\f3\fs18 individualOffsetMean
4193+
\f4\fs20 and
4194+
\f3\fs18 individualOffsetSD
4195+
\f4\fs20 properties) in each target individual. (Note that individual offsets are automatically drawn from these distributions when an individual is created; this re-draws new offset values.) In the second pattern,
41954196
\f3\fs18 offset
41964197
\f4\fs20 is a singleton value; this sets the given offset for each of the specified traits in each target individual. In the third pattern,
41974198
\f3\fs18 offset

VERSIONS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ multitrait branch:
5555
add support in Individual for the individual's offset for each trait
5656
add -(float)offsetForTrait([Nio<Trait> trait = NULL])
5757
add +(void)setOffsetForTrait([Nio<Trait> trait = NULL], [Nif offset = NULL])
58+
draw an individual's trait offsets from the trait individual-offset distributions, at the individual's moment of generation
5859

5960

6061
version 5.1 (Eidos version 4.1):

core/individual.cpp

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ bool Individual::s_any_individual_fitness_scaling_set_ = false;
5353

5454
// individual first, haplosomes later; this is the new multichrom paradigm
5555
// BCH 10/12/2024: Note that this will rarely be called after simulation startup; see NewSubpopIndividual()
56+
// BCH 10/12/2025: Note also that NewSubpopIndividual() will rarely be called in WF models; see the Munge...() methods
5657
Individual::Individual(Subpopulation *p_subpopulation, slim_popsize_t p_individual_index, IndividualSex p_sex, slim_age_t p_age, double p_fitness, float p_mean_parent_age) :
5758
#ifdef SLIMGUI
5859
color_set_(false),
@@ -83,44 +84,69 @@ Individual::Individual(Subpopulation *p_subpopulation, slim_popsize_t p_individu
8384
}
8485

8586
// Set up per-trait information such as phenotype caches and individual offsets
87+
_DrawTraitOffsets();
88+
89+
// Initialize tag values to the "unset" value
90+
tag_value_ = SLIM_TAG_UNSET_VALUE;
91+
tagF_value_ = SLIM_TAGF_UNSET_VALUE;
92+
tagL0_set_ = false;
93+
tagL1_set_ = false;
94+
tagL2_set_ = false;
95+
tagL3_set_ = false;
96+
tagL4_set_ = false;
97+
98+
// Initialize x/y/z to 0.0, only when leak-checking (they show up as used before initialized in Valgrind)
99+
#if SLIM_LEAK_CHECKING
100+
spatial_x_ = 0.0;
101+
spatial_y_ = 0.0;
102+
spatial_z_ = 0.0;
103+
#endif
104+
}
105+
106+
void Individual::_DrawTraitOffsets(void)
107+
{
108+
// Set up per-trait individual-level information such as individual offsets. This is called by
109+
// Individual::Individual(), but also in various other places where individuals are re-used.
110+
111+
// FIXME MULTITRAIT: this will probably be a pain point; maybe we can skip it if offsets have never been changed by the user?
112+
// I imagine a design where there is a bool flag that says "the offsets for this individual have been initialized". This
113+
// would allow a lazy caching scheme; if an offset is queried or set, all the offsets in that individual are then set up
114+
// and the flag is set to indicate that it has been set up. Every tick, offsets will probably be needed for every individual
115+
// in order to calculate phenotypes, so we can't avoid that work altogether. But we can avoid doing it one individual at a
116+
// time, with a lot of setup overhead here to get the traits, get the RNG, etc.; we could do it in bulk for all individuals
117+
// in a species, at the point when we're calculating everybody's phenotypes, which would allow us to do it very quickly.
118+
// The only difficulty I see with such a lazy caching scheme is: if the trait's individual-offset distribution is *changed*
119+
// by the script, then at that moment, the individual offsets of every alive individual need to be initialized using the old
120+
// distribution before it changes, otherwise they will (incorrectly) draw from the new distribution. So that's a little
121+
// tricky, but doable. I'm going to put off doing this until later, though, so as to not get bogged down. BCH 10/12/2025
122+
123+
// FIXME MULTITRAIT: note also that if all trait individual offsets have SD == 0, and thus initialize to a constant, we
124+
// could use a buffer of default trait values that we memcpy() in to each individual's offset buffer. That would be
125+
// a strategy that could easily be used when we bulk-initialize the offsets of all uninitialized individuals, for example.
126+
127+
// FIXME MULTITRAIT: also, _DrawIndividualOffset() looks up the RNG; when doing this work in bulk, we can look up the RNG
128+
// once and then pass it in to DrawIndividualOffset() instead of having it look it up. Much optimization to do in bulk.
129+
86130
Species &species = subpopulation_->species_;
87131
const std::vector<Trait *> &traits = species.Traits();
88132
int trait_count = (int)traits.size();
89133

90134
if (trait_count == 1)
91135
{
92-
// FIXME MULTITRAIT: DefaultOffset() has a branch; maybe better for each trait to have a cached slim_effect_t for it? or maybe not?
93136
offsets_for_traits_ = &offset_for_trait_0_;
94-
offset_for_trait_0_ = traits[0]->DefaultOffset();
137+
offset_for_trait_0_ = traits[0]->DrawIndividualOffset();
95138
}
96139
else if (trait_count == 0)
97140
{
98141
offsets_for_traits_ = nullptr;
99142
}
100143
else
101144
{
102-
// FIXME MULTITRAIT: we could keep a buffer of default trait values in the Species, and just memcpy() here
103145
offsets_for_traits_ = static_cast<slim_effect_t *>(malloc(trait_count * sizeof(slim_effect_t)));
104146

105147
for (int trait_index = 0; trait_index < trait_count; ++trait_index)
106-
offsets_for_traits_[trait_index] = traits[trait_index]->DefaultOffset();
148+
offsets_for_traits_[trait_index] = traits[trait_index]->DrawIndividualOffset();
107149
}
108-
109-
// Initialize tag values to the "unset" value
110-
tag_value_ = SLIM_TAG_UNSET_VALUE;
111-
tagF_value_ = SLIM_TAGF_UNSET_VALUE;
112-
tagL0_set_ = false;
113-
tagL1_set_ = false;
114-
tagL2_set_ = false;
115-
tagL3_set_ = false;
116-
tagL4_set_ = false;
117-
118-
// Initialize x/y/z to 0.0, only when leak-checking (they show up as used before initialized in Valgrind)
119-
#if SLIM_LEAK_CHECKING
120-
spatial_x_ = 0.0;
121-
spatial_y_ = 0.0;
122-
spatial_z_ = 0.0;
123-
#endif
124150
}
125151

126152
Individual::~Individual(void)
@@ -4051,11 +4077,11 @@ EidosValue_SP Individual_Class::ExecuteMethod_setOffsetForTrait(EidosGlobalStrin
40514077

40524078
if (offset_value->Type() == EidosValueType::kValueNULL)
40534079
{
4054-
// pattern 1: setting the default offset value for each trait in one or more individuals
4080+
// pattern 1: drawing a default offset value for each trait in one or more individuals
40554081
for (int64_t trait_index : trait_indices)
40564082
{
40574083
Trait *trait = species->Traits()[trait_index];
4058-
slim_effect_t offset = trait->DefaultOffset();
4084+
slim_effect_t offset = trait->DrawIndividualOffset();
40594085

40604086
for (int individual_index = 0; individual_index < individuals_count; ++individual_index)
40614087
{

core/individual.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ class Individual : public EidosDictionaryUnretained
169169
Individual(Subpopulation *p_subpopulation, slim_popsize_t p_individual_index, IndividualSex p_sex, slim_age_t p_age, double p_fitness, float p_mean_parent_age);
170170
virtual ~Individual(void) override;
171171

172+
void _DrawTraitOffsets(void);
173+
172174
inline __attribute__((always_inline)) void ClearColor(void) {
173175
#ifdef SLIMGUI
174176
// BCH 3/23/2025: color variables now only exist in SLiMgui, to save on memory footprint

core/slim_test.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ int RunSLiMTests(void)
461461
_RunChromosomeTests();
462462
_RunMutationTests();
463463
_RunHaplosomeTests(temp_path);
464+
_RunMultitraitTests();
464465
_RunSubpopulationTests();
465466
_RunIndividualTests();
466467
_RunSubstitutionTests();

core/slim_test.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ extern void _RunGenomicElementTests(void);
4949
extern void _RunChromosomeTests(void);
5050
extern void _RunMutationTests(void);
5151
extern void _RunHaplosomeTests(const std::string &temp_path);
52+
extern void _RunMultitraitTests(void);
5253
extern void _RunSubpopulationTests(void);
5354
extern void _RunIndividualTests(void);
5455
extern void _RunErrorPositionTests(void);

0 commit comments

Comments
 (0)