Skip to content

Commit 01f45fa

Browse files
authored
fix(math): Fix range mapper deadband logic (#423)
* Fix issue in range mapper which could occur if the center and range deadzones overlapped * More explicitly allow uni-directional output mappings when min==center or max==center * Flesh out test cases for floating point precision issues and values around the deadbands It was found that the output could be discontinuous if the center deadzone overlapped with one of the range deadzones. The solution was to better handle the case that there are unidirectional output distributions (e.g. where center==min or center==max) and to better handle how the deadbands are calculated. Build and run `math/example` on QtPy ESP32s3 and ensure the output of `rm4` matches what we expect.
1 parent 25e6ee8 commit 01f45fa

File tree

2 files changed

+47
-13
lines changed

2 files changed

+47
-13
lines changed

components/math/example/main/math_example.cpp

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,49 @@ extern "C" void app_main(void) {
183183
// You can even invert the ouput distribution, and add a deadband around the
184184
// min/max values
185185
espp::FloatRangeMapper rm4({
186-
.center = center,
187-
.center_deadband = deadband,
186+
.center = max, // test uni-directional mapping
187+
.center_deadband = deadband / 2.0f, // test different deadband around center / range
188188
.minimum = min,
189189
.maximum = max,
190190
.range_deadband = deadband,
191191
.invert_output = true,
192192
});
193193
// make a vector of float values min - 10 to max + 10 in increments of 5
194194
std::vector<float> vals;
195-
for (float v = min - 10; v <= max + 10; v += 5) {
195+
static constexpr float increment = deadband / 3.0f;
196+
static constexpr float oob_range = deadband * 3.0f;
197+
for (float v = min - oob_range; v <= max + oob_range; v += increment) {
196198
vals.push_back(v);
197199
}
200+
// ensure that very small values around center, max, and min are mapped to
201+
// 0, 1, and -1 respectively
202+
vals.push_back(center - 0.0001f);
203+
vals.push_back(center - 0.00001f);
204+
vals.push_back(center - 0.000001f);
205+
vals.push_back(center - std::numeric_limits<float>::epsilon());
206+
vals.push_back(center + 0.0001f);
207+
vals.push_back(center + 0.00001f);
208+
vals.push_back(center + 0.000001f);
209+
vals.push_back(center + std::numeric_limits<float>::epsilon());
210+
vals.push_back(max - 0.0001f);
211+
vals.push_back(max - 0.00001f);
212+
vals.push_back(max - 0.000001f);
213+
vals.push_back(max - std::numeric_limits<float>::epsilon());
214+
vals.push_back(max + 0.0001f);
215+
vals.push_back(max + 0.00001f);
216+
vals.push_back(max + 0.000001f);
217+
vals.push_back(max + std::numeric_limits<float>::epsilon());
218+
vals.push_back(min - 0.0001f);
219+
vals.push_back(min - 0.00001f);
220+
vals.push_back(min - 0.000001f);
221+
vals.push_back(min - std::numeric_limits<float>::epsilon());
222+
vals.push_back(min + 0.0001f);
223+
vals.push_back(min + 0.00001f);
224+
vals.push_back(min + 0.000001f);
225+
vals.push_back(min + std::numeric_limits<float>::epsilon());
226+
227+
std::sort(vals.begin(), vals.end());
228+
198229
// test the mapping and unmapping
199230
fmt::print(
200231
"% value, mapped [0;255] to [-1;1], unmapped [-1;1] to [0;255], mapped [0;255] to "

components/math/include/range_mapper.hpp

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,21 +194,24 @@ template <typename T> class RangeMapper {
194194
T calibrated{0};
195195
// compare against center
196196
calibrated = clamped - center_;
197-
bool positive_input = calibrated >= 0;
197+
bool min_eq_center = minimum_ == center_;
198+
bool max_eq_center = maximum_ == center_;
198199
bool within_center_deadband = std::abs(calibrated) < center_deadband_;
199-
bool within_range_deadband =
200-
clamped >= maximum_ - range_deadband_ || clamped <= minimum_ + range_deadband_;
200+
// Ensure we handle the case that the center is equal to the min or max,
201+
// which may happen if the user wants a uni-directional output (e.g. [0,1]
202+
// instead of [-1,1]). In this case, we only apply the center deadband, not
203+
// the range deadband.
204+
bool within_max_deadband = !max_eq_center && clamped >= (maximum_ - range_deadband_);
205+
bool within_min_deadband = !min_eq_center && clamped <= (minimum_ + range_deadband_);
201206
if (within_center_deadband) {
202-
// if it's within the center deadband, return the output center
203207
return output_center_;
204-
} else if (within_range_deadband) {
205-
// if it's within the range deadband around the min/max, return the output
206-
// min/max, taking into account the output inversion
207-
return positive_input ? invert_output_ ? output_min_ : output_max_
208-
: invert_output_ ? output_max_
209-
: output_min_;
208+
} else if (within_min_deadband) {
209+
return invert_output_ ? output_max_ : output_min_;
210+
} else if (within_max_deadband) {
211+
return invert_output_ ? output_min_ : output_max_;
210212
}
211213

214+
bool positive_input = calibrated >= 0;
212215
// remove the deadband from the calibrated value
213216
calibrated = positive_input ? calibrated - center_deadband_ : calibrated + center_deadband_;
214217

0 commit comments

Comments
 (0)