Skip to content

fix(transforms): apply MovingAverage multiplier to the output#1004

Merged
mairas merged 1 commit into
mainfrom
fix/1003-moving-average-multiplier
Jun 15, 2026
Merged

fix(transforms): apply MovingAverage multiplier to the output#1004
mairas merged 1 commit into
mainfrom
fix/1003-moving-average-multiplier

Conversation

@mairas

@mairas mairas commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Bug

MovingAverage's documented contract is y = multiplier * (1/n) * Σx ("Moving average will be multiplied by multiplier before it is output"). The implementation didn't honor it:

  • the seeded first sample was emitted unscaled (output_ = input);
  • subsequent updates applied the multiplier to the incremental delta (±multiplier * value / n), so for a steady input the multiplier cancelled entirely and only affected the transient.

multiplier = 1 (the default) behaved correctly as a plain moving average, but any multiplier ≠ 1 — e.g. the scale used in the fuel-level and milone-level examples — produced wrong output.

Fix

Maintain a running sum_ of the buffered samples and compute output_ = multiplier_ * sum_ / sample_size_ on every sample, including the seeded first one. This matches the documented formula exactly.

Tests

New test/system/test_moving_average_multiplier/:

  • multiplier scales the seeded first sample;
  • multiplier applies to a steady input stream (the case that previously cancelled);
  • output tracks multiplier × windowed mean across several samples;
  • multiplier = 1 is still a plain moving average.

Compile-verified locally with pio test --without-uploading --without-testing -e arduino_esp32 (passes). As with the rest of the suite, CI compiles tests but does not execute them; assertions were traced against the new logic and should be run on a device to confirm.

Closes #1003.

@mairas

mairas commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Automated review

Verdict: Approve. The fix correctly implements the documented contract y = multiplier * (1/n) * Σx, and all four test assertions match a hand-trace of the new logic. (Source-tracing only — CI compiles tests but does not execute them; I did not run them.)

Correctness (verified by source-tracing)

  • Seeded first sample: init branch sets sum_ = sample_size_ * input, so output_ = multiplier_ * sum_ / n = multiplier * input. Previously emitted unscaled (output_ = input). Fixed.
  • Steady input: sum_ += input - buf_[ptr_] leaves sum_ unchanged when input is constant, so output_ stays multiplier * mean. This is the case where the old incremental-delta form cancelled the multiplier. Fixed.
  • Changing window: traced MovingAverage(2, 2.0) for set(10),set(20),set(30) → 20, 30, 50, and MovingAverage(4, 1.0) for set(4),set(8),set(8),set(8) → 4, 5, 6, 7. Both match. The steady-input test MovingAverage(4, 3.0)×6 → 15.0 also matches.
  • All four assertions in test/system/test_moving_average_multiplier/ match the traced values.

State / numerics

  • sum_ has in-class initializer = 0, but the init branch overwrites it unconditionally on first set(), so there is no reliance on stale state. from_json() resets initialized_ = false on a sample-size change, forcing re-seed and a fresh sum_ recompute on the next set() — no stale carry-over across reconfiguration.
  • No integer-division pitfall: sum_ is float, so sum_ / sample_size_ is float division despite sample_size_ being int.
  • Floating-point drift: sum_ accumulates via add/subtract deltas rather than recomputing from the buffer each call — identical drift characteristics to the previous running-output_ approach, so this is not a regression.

Compatibility

  • multiplier = 1 (default): new output = sum_/n (true mean) equals the old behavior in steady state. Default behavior unchanged. Existing multiplier != 1 users (e.g. fuel_level_sensor, milone_level_sensor scale) now get correct scaling — this is the intended bug fix, not a silent behavior change for correct callers.

Conventions / minimality

No blocking issues. One non-blocking note already acknowledged in the PR description: assertions are source-traced, not device-executed.

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>
@mairas mairas force-pushed the fix/1003-moving-average-multiplier branch from 3695d17 to 79688a7 Compare June 15, 2026 21:22
@mairas mairas merged commit 79a90e4 into main Jun 15, 2026
24 checks passed
@mairas mairas deleted the fix/1003-moving-average-multiplier branch June 15, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MovingAverage multiplier has ill-defined effect (unscaled first sample, cancels for constant input)

1 participant