Skip to content

Commit 3695d17

Browse files
mairasclaude
andcommitted
fix(transforms): apply MovingAverage multiplier to the output
The documented contract is y = multiplier * mean, but set() applied the multiplier to incremental deltas, so it cancelled for a steady input and skipped the seeded first sample. Maintain a running sum and output multiplier * sum / sample_size, matching the formula for every sample. Closes #1003. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 624cfa6 commit 3695d17

3 files changed

Lines changed: 96 additions & 8 deletions

File tree

src/sensesp/transforms/moving_average.cpp

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,19 @@ MovingAverage::MovingAverage(int sample_size, float multiplier,
1717
}
1818

1919
void MovingAverage::set(const float& input) {
20-
// So the first value to be included in the average doesn't default to 0.0
2120
if (!initialized_) {
21+
// Seed the whole buffer with the first value so the average doesn't start
22+
// out diluted by zeros.
2223
buf_.assign(sample_size_, input);
23-
output_ = input;
24+
sum_ = static_cast<float>(sample_size_) * input;
2425
initialized_ = true;
2526
} else {
26-
// Subtract 1/nth of the oldest value and add 1/nth of the newest value
27-
output_ += -multiplier_ * buf_[ptr_] / sample_size_;
28-
output_ += multiplier_ * input / sample_size_;
29-
30-
// Save the most recent input, then advance to the next storage location.
31-
// When storage location n is reached, start over again at 0.
27+
// Swap the oldest buffered value out of the running sum for the newest.
28+
sum_ += input - buf_[ptr_];
3229
buf_[ptr_] = input;
3330
ptr_ = (ptr_ + 1) % sample_size_;
3431
}
32+
output_ = multiplier_ * sum_ / sample_size_;
3533
notify();
3634
}
3735

src/sensesp/transforms/moving_average.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class MovingAverage : public FloatTransform {
4444
int ptr_ = 0;
4545
int sample_size_;
4646
float multiplier_;
47+
float sum_ = 0;
4748
bool initialized_;
4849
};
4950

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @file moving_average_multiplier_test.cpp
3+
* @brief Regression tests for MovingAverage's multiplier (#1003).
4+
*
5+
* The documented contract is y = multiplier * (1/n) * sum(last n inputs):
6+
* the multiplier scales the averaged output, including the seeded first
7+
* sample and a steady input stream.
8+
*/
9+
10+
#include <Arduino.h>
11+
12+
#include "sensesp/system/lambda_consumer.h"
13+
#include "sensesp/transforms/moving_average.h"
14+
#include "unity.h"
15+
16+
using namespace sensesp;
17+
18+
void test_multiplier_scales_seeded_first_sample() {
19+
MovingAverage ma(2, 2.0); // output = 2.0 * mean
20+
21+
float received = -1.0f;
22+
LambdaConsumer<float> consumer([&received](float v) { received = v; });
23+
ma.connect_to(&consumer);
24+
25+
// First sample seeds the whole buffer (mean == input), scaled by 2.0.
26+
ma.set(10.0f);
27+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 20.0f, received);
28+
}
29+
30+
void test_multiplier_applies_to_steady_input() {
31+
MovingAverage ma(4, 3.0); // output = 3.0 * mean
32+
33+
float received = -1.0f;
34+
LambdaConsumer<float> consumer([&received](float v) { received = v; });
35+
ma.connect_to(&consumer);
36+
37+
// A constant input of 5.0 has mean 5.0; the multiplier must still apply.
38+
for (int i = 0; i < 6; i++) {
39+
ma.set(5.0f);
40+
}
41+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 15.0f, received);
42+
}
43+
44+
void test_multiplier_tracks_windowed_mean() {
45+
MovingAverage ma(2, 2.0);
46+
47+
float received = -1.0f;
48+
LambdaConsumer<float> consumer([&received](float v) { received = v; });
49+
ma.connect_to(&consumer);
50+
51+
ma.set(10.0f); // buffer [10,10], mean 10 -> 20
52+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 20.0f, received);
53+
54+
ma.set(20.0f); // buffer [20,10], mean 15 -> 30
55+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 30.0f, received);
56+
57+
ma.set(30.0f); // buffer [20,30], mean 25 -> 50
58+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 50.0f, received);
59+
}
60+
61+
void test_default_multiplier_is_plain_average() {
62+
MovingAverage ma(4, 1.0);
63+
64+
float received = -1.0f;
65+
LambdaConsumer<float> consumer([&received](float v) { received = v; });
66+
ma.connect_to(&consumer);
67+
68+
// Buffer initializes to all 4.0; push three 8.0s: (8+8+8+4)/4 = 7.0.
69+
ma.set(4.0f);
70+
ma.set(8.0f);
71+
ma.set(8.0f);
72+
ma.set(8.0f);
73+
TEST_ASSERT_FLOAT_WITHIN(0.0001f, 7.0f, received);
74+
}
75+
76+
void setup() {
77+
delay(2000);
78+
79+
UNITY_BEGIN();
80+
81+
RUN_TEST(test_multiplier_scales_seeded_first_sample);
82+
RUN_TEST(test_multiplier_applies_to_steady_input);
83+
RUN_TEST(test_multiplier_tracks_windowed_mean);
84+
RUN_TEST(test_default_multiplier_is_plain_average);
85+
86+
UNITY_END();
87+
}
88+
89+
void loop() {}

0 commit comments

Comments
 (0)