Skip to content

Commit 3ccddf6

Browse files
committed
fix #550, add filter() function for running means and such
1 parent 7da9c0a commit 3ccddf6

7 files changed

Lines changed: 199 additions & 1 deletion

File tree

EidosScribe/EidosHelpFunctions.rtf

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,44 @@
915915
\f3\fs20 with a matrix or array argument, because the desired behavior in that case has not yet been implemented.\
916916
\pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0
917917

918-
\f1\fs18 \cf0 \kerning1\expnd0\expndtw0 (+$)max(+\'a0x, ...)\
918+
\f1\fs18 \cf2 \kerning1\expnd0\expndtw0 (float)filter(numeric\'a0x, float\'a0filter)\
919+
\pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0
920+
921+
\f3\fs20 \cf2 Returns the result of convolving
922+
\f1\fs18 x
923+
\f3\fs20 with
924+
\f1\fs18 filter
925+
\f3\fs20 . The returned vector will be the same length as
926+
\f1\fs18 x
927+
\f3\fs20 . The convolution is performed by centering
928+
\f1\fs18 filter
929+
\f3\fs20 on each position of
930+
\f1\fs18 x
931+
\f3\fs20 to produce a corresponding result element that is the sum over the products of each
932+
\f1\fs18 filter
933+
\f3\fs20 value with each
934+
\f1\fs18 x
935+
\f3\fs20 value within the filter\'92s range. If the filter, centered over a given value of
936+
\f1\fs18 x
937+
\f3\fs20 , extends beyond the end of
938+
\f1\fs18 x
939+
\f3\fs20 the corresponding result element will be
940+
\f1\fs18 NAN
941+
\f3\fs20 since its value is undefined. The length of
942+
\f1\fs18 filter
943+
\f3\fs20 is required to be odd, so that the filter has a central value (and can thus be centered over each value of
944+
\f1\fs18 x
945+
\f3\fs20 ).\
946+
This function is useful for computing running means and similar transformations of an input vector,. For a simple running mean of width
947+
\f1\fs18 w
948+
\f3\fs20 , pass r
949+
\f1\fs18 ep(1/w, w)
950+
\f3\fs20 for
951+
\f1\fs18 filter
952+
\f3\fs20 .\
953+
\pard\pardeftab720\li720\fi-446\ri720\sb180\sa60\partightenfactor0
954+
955+
\f1\fs18 \cf0 (+$)max(+\'a0x, ...)\
919956
\pard\pardeftab720\li547\ri720\sb60\sa60\partightenfactor0
920957

921958
\f3\fs20 \cf0 Returns the

QtSLiM/help/EidosHelpFunctions.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@
118118
<p class="p5"><span class="s5">Returns the <b>sample Pearson’s correlation coefficient</b> between </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5">, usually denoted <i>r</i>.<span class="Apple-converted-space">  </span>The sizes of </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5"> must be identical.<span class="Apple-converted-space">  </span>If </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5"> have a size of </span><span class="s6">0</span><span class="s5"> or </span><span class="s6">1</span><span class="s5">, the return value will be </span><span class="s6">NULL</span><span class="s5">.<span class="Apple-converted-space">  </span>At present it is illegal to call </span><span class="s6">cor()</span><span class="s5"> with a matrix or array argument, because the desired behavior in that case has not yet been implemented.</span></p>
119119
<p class="p4"><span class="s5">(float$)cov(numeric x, numeric y)</span></p>
120120
<p class="p5"><span class="s5">Returns the <b>corrected sample covariance</b> between </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5">.<span class="Apple-converted-space">  </span>The sizes of </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5"> must be identical.<span class="Apple-converted-space">  </span>If </span><span class="s6">x</span><span class="s5"> and </span><span class="s6">y</span><span class="s5"> have a size of </span><span class="s6">0</span><span class="s5"> or </span><span class="s6">1</span><span class="s5">, the return value will be </span><span class="s6">NULL</span><span class="s5">.<span class="Apple-converted-space">  </span>At present it is illegal to call </span><span class="s6">cov()</span><span class="s5"> with a matrix or array argument, because the desired behavior in that case has not yet been implemented.</span></p>
121+
<p class="p4">(float)filter(numeric x, float filter)</p>
122+
<p class="p5">Returns the result of convolving <span class="s2">x</span> with <span class="s2">filter</span>.<span class="Apple-converted-space">  </span>The returned vector will be the same length as <span class="s2">x</span>.<span class="Apple-converted-space">  </span>The convolution is performed by centering <span class="s2">filter</span> on each position of <span class="s2">x</span> to produce a corresponding result element that is the sum over the products of each <span class="s2">filter</span> value with each <span class="s2">x</span> value within the filter’s range.<span class="Apple-converted-space">  </span>If the filter, centered over a given value of <span class="s2">x</span>, extends beyond the end of <span class="s2">x</span> the corresponding result element will be <span class="s2">NAN</span> since its value is undefined.<span class="Apple-converted-space">  </span>The length of <span class="s2">filter</span> is required to be odd, so that the filter has a central value (and can thus be centered over each value of <span class="s2">x</span>).</p>
123+
<p class="p5">This function is useful for computing running means and similar transformations of an input vector,.<span class="Apple-converted-space">  </span>For a simple running mean of width <span class="s2">w</span>, pass r<span class="s2">ep(1/w, w)</span> for <span class="s2">filter</span>.</p>
121124
<p class="p2">(+$)max(+ x, ...)</p>
122125
<p class="p3">Returns the <b>maximum</b> of <span class="s2">x</span> and the other arguments supplied: the single greatest value contained by all of them.<span class="Apple-converted-space">  </span>All of the arguments must be the same type as <span class="s2">x</span>, and the return type will match that of <span class="s2">x</span><span class="s3">.</span><span class="Apple-converted-space">  </span>If all of the arguments have a size of <span class="s2">0</span>, the return value will be <span class="s2">NULL</span>; note that this means that <span class="s2">max(x, max(y))</span> may produce an error, if <span class="s2">max(y)</span> is <span class="s2">NULL</span>, in cases where <span class="s2">max(x, y)</span> does not.</p>
123126
<p class="p2">(float$)mean(lif<span class="s1"> </span>x)</p>

VERSIONS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ development head (in the master branch):
2626
fix #534, support uniparentally-transmitted chromosomes in hermaphrodite populations
2727
add new Plot legendTitleEntry() method for making a title line in a plot legend
2828
click-drag in the SLiMgui script view's line number area now drag-selects whole lines
29+
add filter() function to Eidos, which is essentially a subset of R's filter(): (float)filter(numeric x, float filter)
2930

3031

3132
version 5.0 (Eidos version 4.0):

eidos/eidos_functions.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const std::vector<EidosFunctionSignature_CSP> &EidosInterpreter::BuiltInFunction
116116

117117
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("cor", Eidos_ExecuteFunction_cor, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddNumeric("x")->AddNumeric("y"));
118118
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("cov", Eidos_ExecuteFunction_cov, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddNumeric("x")->AddNumeric("y"));
119+
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("filter", Eidos_ExecuteFunction_filter, kEidosValueMaskFloat))->AddNumeric("x")->AddFloat("filter"));
119120
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("max", Eidos_ExecuteFunction_max, kEidosValueMaskAnyBase | kEidosValueMaskSingleton))->AddAnyBase("x")->AddEllipsis());
120121
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("mean", Eidos_ExecuteFunction_mean, kEidosValueMaskFloat | kEidosValueMaskSingleton))->AddLogicalEquiv("x"));
121122
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("min", Eidos_ExecuteFunction_min, kEidosValueMaskAnyBase | kEidosValueMaskSingleton))->AddAnyBase("x")->AddEllipsis());

eidos/eidos_functions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ EidosValue_SP Eidos_ExecuteFunction_trunc(const std::vector<EidosValue_SP> &p_ar
8484
// statistics functions
8585
EidosValue_SP Eidos_ExecuteFunction_cor(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
8686
EidosValue_SP Eidos_ExecuteFunction_cov(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
87+
EidosValue_SP Eidos_ExecuteFunction_filter(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
8788
EidosValue_SP Eidos_ExecuteFunction_max(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
8889
EidosValue_SP Eidos_ExecuteFunction_mean(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
8990
EidosValue_SP Eidos_ExecuteFunction_min(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);

eidos/eidos_functions_stats.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,141 @@ EidosValue_SP Eidos_ExecuteFunction_cov(const std::vector<EidosValue_SP> &p_argu
140140
return result_SP;
141141
}
142142

143+
// (float)filter(numeric x, float filter)
144+
EidosValue_SP Eidos_ExecuteFunction_filter(const std::vector<EidosValue_SP> &p_arguments, __attribute__((unused)) EidosInterpreter &p_interpreter)
145+
{
146+
// this is patterned after the R function filter(), but only for method="convolution", sides=2, circular=F
147+
// so for now we support only a centered filter convolved over x; values outside x are assumed to be NAN
148+
149+
EidosValue *x_value = p_arguments[0].get();
150+
EidosValue *filter_value = p_arguments[1].get();
151+
int x_count = x_value->Count();
152+
int filter_count = filter_value->Count();
153+
154+
// the maximum filter length is arbitrary, but seems like a good idea to flag weird bugs?
155+
if ((filter_count <= 0) | (filter_count > 999) | (filter_count % 2 == 0))
156+
EIDOS_TERMINATION << "ERROR (Eidos_ExecuteFunction_filter): function filter() requires filter to have a length that is odd and within the interval [1, 999]." << EidosTerminate(nullptr);
157+
158+
// half rounded down; e.g., for a filter of length 5, this is 2; this is the number of NANs at the
159+
// start/end of the result, since the filter extends past the end of x for this many positions
160+
int half_filter = filter_count / 2;
161+
162+
// the result is the same length as x, in all cases
163+
EidosValue_Float *float_result = (new (gEidosValuePool->AllocateChunk()) EidosValue_Float())->resize_no_initialize(x_count);
164+
EidosValue_SP result_SP(float_result);
165+
double *result_data = float_result->FloatData_Mutable();
166+
167+
if (x_count == 0)
168+
return result_SP;
169+
170+
if (x_count < filter_count)
171+
{
172+
for (int pos = 0; pos < x_count; ++pos)
173+
result_data[pos] = std::numeric_limits<double>::quiet_NaN();
174+
}
175+
176+
// get x data and filter data
177+
const double *filter_data = filter_value->FloatData();
178+
179+
// we test here for a simple moving average, with equal weights summing to 1.0, to special-case it
180+
bool is_simple_moving_average = true;
181+
182+
for (int index = 0; index < filter_count; ++index)
183+
{
184+
if (std::abs(filter_data[index] - (1.0 / filter_count)) > 1e-15) // 1e-15 is a roundoff epsilon
185+
{
186+
is_simple_moving_average = false;
187+
break;
188+
}
189+
}
190+
191+
// the half-filter length at the start fills with NAN
192+
for (int pos = 0; pos < half_filter; ++pos)
193+
result_data[pos] = std::numeric_limits<double>::quiet_NaN();
194+
195+
// now we branch depending on whether x is integer or float
196+
if (x_value->Type() == EidosValueType::kValueFloat)
197+
{
198+
const double *x_data = x_value->FloatData();
199+
200+
if (is_simple_moving_average)
201+
{
202+
// the first position after the half-filter length sets up a moving total
203+
double moving_total = 0.0;
204+
205+
for (int pos = 0; pos < filter_count; ++pos)
206+
moving_total += x_data[pos];
207+
208+
result_data[half_filter] = moving_total / filter_count;
209+
210+
// the remaining non-NAN positions modify the moving total
211+
for (int pos = half_filter + 1; pos < x_count - half_filter; ++pos)
212+
{
213+
moving_total -= x_data[pos - half_filter - 1];
214+
moving_total += x_data[pos + half_filter];
215+
216+
result_data[pos] = moving_total / filter_count;
217+
}
218+
}
219+
else
220+
{
221+
// we compute the filter over the appropriate range of x at each position
222+
for (int pos = half_filter; pos < x_count - half_filter; ++pos)
223+
{
224+
double filter_total = 0.0;
225+
226+
for (int filter_pos = 0; filter_pos < filter_count; filter_pos++)
227+
filter_total += filter_data[filter_pos] * x_data[filter_pos + pos - half_filter];
228+
229+
result_data[pos] = filter_total;
230+
}
231+
}
232+
}
233+
else
234+
{
235+
const int64_t *x_data = x_value->IntData();
236+
237+
if (is_simple_moving_average)
238+
{
239+
// the first position after the half-filter length sets up a moving total
240+
double moving_total = 0.0;
241+
242+
for (int pos = 0; pos < filter_count; ++pos)
243+
moving_total += x_data[pos];
244+
245+
result_data[half_filter] = moving_total / filter_count;
246+
247+
// the remaining non-NAN positions modify the moving total
248+
for (int pos = half_filter + 1; pos < x_count - half_filter; ++pos)
249+
{
250+
moving_total -= x_data[pos - half_filter - 1];
251+
moving_total += x_data[pos + half_filter];
252+
253+
result_data[pos] = moving_total / filter_count;
254+
}
255+
}
256+
else
257+
{
258+
// we compute the filter over the appropriate range of x at each position
259+
for (int pos = half_filter; pos < x_count - half_filter; ++pos)
260+
{
261+
double filter_total = 0.0;
262+
263+
for (int filter_pos = 0; filter_pos < filter_count; filter_pos++)
264+
filter_total += filter_data[filter_pos] * x_data[filter_pos + pos - half_filter];
265+
266+
result_data[pos] = filter_total;
267+
}
268+
}
269+
}
270+
271+
// the half-filter length at the end fills with NAN
272+
for (int pos = x_count - half_filter; pos < x_count; ++pos)
273+
result_data[pos] = std::numeric_limits<double>::quiet_NaN();
274+
275+
return result_SP;
276+
}
277+
143278
// (+$)max(+ x, ...)
144279
EidosValue_SP Eidos_ExecuteFunction_max(const std::vector<EidosValue_SP> &p_arguments, __attribute__((unused)) EidosInterpreter &p_interpreter)
145280
{

eidos/eidos_test_functions_statistics.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ void _RunFunctionStatisticsTests_a_through_p(void)
7878
EidosAssertScriptSuccess_NULL("cov(float(0), float(0));");
7979
EidosAssertScriptRaise("cov(string(0), string(0));", 0, "cannot be type");
8080

81+
// filter()
82+
EidosAssertScriptRaise("filter(1.0:10, float(0));", 0, "within the interval [1,");
83+
EidosAssertScriptRaise("filter(1.0:10, 1.0:2);", 0, "length that is odd");
84+
EidosAssertScriptSuccess_L("x = runif(100); identical(x, filter(x, 1.0));", true);
85+
EidosAssertScriptSuccess_L("x = runif(100); identical(x * 2.0, filter(x, 2.0));", true);
86+
EidosAssertScriptSuccess_L("x = runif(100); identical(x * -2.5, filter(x, -2.5));", true);
87+
EidosAssertScriptSuccess_L("x = rep(NAN, 10); identical(x, filter(x, 1.0));", true);
88+
EidosAssertScriptSuccess_FV("filter(1.0:10, rep(1/3, 3));", {std::numeric_limits<double>::quiet_NaN(), 2, 3, 4, 5, 6, 7, 8, 9, std::numeric_limits<double>::quiet_NaN()});
89+
EidosAssertScriptSuccess_FV("filter(1.0:10, rep(1/5, 5));", {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(), 3, 4, 5, 6, 7, 8, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()});
90+
EidosAssertScriptSuccess_FV("filter(1.0:10, rep(1.0, 5));", {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(), 15, 20, 25, 30, 35, 40, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()});
91+
92+
EidosAssertScriptRaise("filter(1:10, float(0));", 0, "within the interval [1,");
93+
EidosAssertScriptRaise("filter(1:10, 1.0:2);", 0, "length that is odd");
94+
EidosAssertScriptSuccess_L("x = rdunif(100, -100, 100); identical(x * 1.0, filter(x, 1.0));", true);
95+
EidosAssertScriptSuccess_L("x = rdunif(100, -100, 100); identical(x * 2.0, filter(x, 2.0));", true);
96+
EidosAssertScriptSuccess_L("x = rdunif(100); identical(x * -2.5, filter(x, -2.5));", true);
97+
EidosAssertScriptSuccess_FV("filter(1:10, rep(1/3, 3));", {std::numeric_limits<double>::quiet_NaN(), 2, 3, 4, 5, 6, 7, 8, 9, std::numeric_limits<double>::quiet_NaN()});
98+
EidosAssertScriptSuccess_FV("filter(1:10, rep(1/5, 5));", {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(), 3, 4, 5, 6, 7, 8, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()});
99+
EidosAssertScriptSuccess_FV("filter(1:10, rep(1.0, 5));", {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(), 15, 20, 25, 30, 35, 40, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()});
100+
81101
// max()
82102
EidosAssertScriptSuccess_L("max(T);", true);
83103
EidosAssertScriptSuccess_I("max(3);", 3);

0 commit comments

Comments
 (0)