Skip to content

Commit 92d1347

Browse files
authored
fix(math): update range mapper implementation (#229)
* Update range mapper implementation so that the scaling starts at the deadband edge (reduce discontinuities in standard output range) * Update range mapper implementation to fix bugs arising from comparing to T(0) when it should have instead been comparing against center_ or output_center_ * Update math example to have better range mapper test - and one which outputs csv for easier plotting / checking * Update range mapper docstrings to have more text and warning about input inversion
1 parent 775fb3c commit 92d1347

File tree

2 files changed

+91
-40
lines changed

2 files changed

+91
-40
lines changed

components/math/example/main/math_example.cpp

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -115,52 +115,68 @@ extern "C" void app_main(void) {
115115
logger.info("=== range mapper ===");
116116
{
117117
//! [range_mapper example]
118+
static constexpr float deadband = 12.0f;
119+
static constexpr float min = 0.0f;
120+
static constexpr float center = 127.0f;
121+
static constexpr float max = 255.0f;
118122
// Default will have output range [-1, 1]
119-
espp::RangeMapper<float> rm({.center = 127, .deadband = 12, .minimum = 0, .maximum = 255});
123+
espp::RangeMapper<float> rm(
124+
{.center = center, .deadband = deadband, .minimum = min, .maximum = max});
120125
// You can explicitly set output center/range. In this case the output will
121126
// be in the range [0, 1024]
122-
espp::RangeMapper<float> rm2({.center = 127,
123-
.deadband = 12,
124-
.minimum = 0,
125-
.maximum = 255,
127+
espp::RangeMapper<float> rm2({.center = center,
128+
.deadband = deadband,
129+
.minimum = min,
130+
.maximum = max,
126131
.output_center = 512,
127132
.output_range = 512});
128133
// You can also invert the input distribution, such that input values are
129134
// compared against the input min/max instead of input center. NOTE: this
130135
// also showcases the use of a non-centered input distribution.
131-
espp::FloatRangeMapper rm3({.center = 0,
132-
.deadband = 12,
133-
.minimum = 0,
134-
.maximum = 255,
136+
espp::FloatRangeMapper rm3({.center = center,
137+
.deadband = deadband,
138+
.minimum = min,
139+
.maximum = max,
135140
.invert_input = true,
136-
.output_center = 0,
137-
.output_range = 1024});
141+
.output_center = 512,
142+
.output_range = 512});
138143
// You can even invert the ouput distribution
139144
espp::FloatRangeMapper rm4({
140-
.center = 127,
141-
.deadband = 12,
142-
.minimum = 0,
143-
.maximum = 255,
145+
.center = center,
146+
.deadband = deadband,
147+
.minimum = min,
148+
.maximum = max,
144149
.invert_output = true,
145150
});
146-
auto vals =
147-
std::array<float, 14>{-10, 0, 10, 50, 100, 120, 127, 135, 150, 200, 240, 250, 255, 275};
151+
auto vals = std::vector<float>{min - 10, min, min + 5, min + 10, min + deadband,
152+
// should show as approx -.66 and -.33
153+
min + (center - deadband - min) * .33f,
154+
min + (center - deadband - min) * .66f, center - deadband,
155+
center - 7, center, center + 7, center + deadband,
156+
// should show as approx .33 and .66
157+
center + deadband + (max - center - deadband) * .33f,
158+
center + deadband + (max - center - deadband) * .66f,
159+
max - deadband, max - 10, max - 5, max, max + 10};
148160
// test the mapping and unmapping
149161
fmt::print("Mapping [0, 255] -> [-1, 1]\n");
162+
fmt::print("% value, mapped, unmapped\n");
150163
for (const auto &v : vals) {
151-
fmt::print("{} -> {} -> {} \n", v, rm.map(v), rm.unmap(rm.map(v)));
164+
fmt::print("{}, {}, {}\n", v, rm.map(v), rm.unmap(rm.map(v)));
152165
}
153166
fmt::print("Mapping [0, 255] -> [0, 1024]\n");
167+
fmt::print("% value, mapped, unmapped\n");
154168
for (const auto &v : vals) {
155-
fmt::print("{} -> {} -> {} \n", v, rm2.map(v), rm2.unmap(rm2.map(v)));
169+
fmt::print("{}, {}, {}\n", v, rm2.map(v), rm2.unmap(rm2.map(v)));
156170
}
157171
fmt::print("Mapping Inverted [0, 255] -> [1024, 0]\n");
172+
fmt::print("% value, mapped, unmapped\n");
158173
for (const auto &v : vals) {
159-
fmt::print("{} -> {} -> {} \n", v, rm3.map(v), rm3.unmap(rm3.map(v)));
174+
fmt::print("{}, {}, {}\n", v, rm3.map(v), rm3.unmap(rm3.map(v)));
160175
}
161176
fmt::print("Mapping [0, 255] -> Inverted [1, -1]\n");
177+
fmt::print("% value, mapped, unmapped\n");
162178
for (const auto &v : vals) {
163-
fmt::print("{} -> {} -> {} \n", v, rm4.map(v), rm4.unmap(rm4.map(v)));
179+
fmt::print("{}, {}, {}\n", v, rm4.map(v), rm4.unmap(rm4.map(v)));
164180
}
165181
//! [range_mapper example]
166182
}

components/math/include/range_mapper.hpp

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@ template <typename T> class RangeMapper {
4545
T deadband; /**< Deadband amount around (+-) the center for which output will be 0. */
4646
T minimum; /**< Minimum value for the input range. */
4747
T maximum; /**< Maximum value for the input range. */
48-
bool invert_input{
49-
false}; /**< Whether to invert the input distribution (default false). @note If true will
50-
compute the input relative to min/max instead of to center. */
51-
T output_center{T(0)}; /**< The center for the output. Default 0. */
48+
bool invert_input{false}; /**< Whether to invert the input distribution (default false).
49+
@note If true will compute the input relative to min/max
50+
instead of to center. @warning This introduces a
51+
discontinuity at the center value and ambiguity around the
52+
min/max values when un-mapping back to the input
53+
distribution. For these reasons this setting is not
54+
recommended and may be replaced in future revisions. */
55+
T output_center{T(0)}; /**< The center for the output. Default 0. */
5256
T output_range{T(1)}; /**< The range (+/-) from the center for the output. Default 1. @note Will
5357
be passed through std::abs() to ensure it is positive. */
5458
bool invert_output{
@@ -89,8 +93,8 @@ template <typename T> class RangeMapper {
8993
output_range_ = std::abs(config.output_range);
9094
output_min_ = output_center_ - output_range_;
9195
output_max_ = output_center_ + output_range_;
92-
pos_range_ = (maximum_ - center_) / output_range_;
93-
neg_range_ = std::abs(minimum_ - center_) / output_range_;
96+
pos_range_ = (maximum_ - (center_ + deadband_)) / output_range_;
97+
neg_range_ = (center_ - deadband_ - minimum_) / output_range_;
9498
invert_output_ = config.invert_output;
9599
}
96100

@@ -138,41 +142,72 @@ template <typename T> class RangeMapper {
138142
T map(const T &v) const {
139143
T clamped = std::clamp(v, minimum_, maximum_);
140144
T calibrated{0};
145+
bool positive_input = clamped >= center_;
141146
if (invert_input_) {
142147
// if we invert the input, then we are comparing against the min/max
143-
calibrated = clamped >= T(0) ? maximum_ - clamped : minimum_ - clamped;
148+
calibrated = positive_input ? maximum_ - clamped : minimum_ - clamped;
149+
// if it's within the deadband, return the output center
150+
if (std::abs(calibrated) < deadband_) {
151+
return output_center_;
152+
}
153+
// remove the deadband from the calibrated value
154+
calibrated = positive_input ? calibrated + deadband_ : calibrated - deadband_;
144155
} else {
145156
// normally we compare against center
146157
calibrated = clamped - center_;
158+
// if it's within the deadband, return the output center
159+
if (std::abs(calibrated) < deadband_) {
160+
return output_center_;
161+
}
162+
// remove the deadband from the calibrated value
163+
calibrated = positive_input ? calibrated - deadband_ : calibrated + deadband_;
147164
}
148-
if (std::abs(calibrated) < deadband_) {
149-
return output_center_;
150-
}
151-
T output = calibrated >= T(0) ? calibrated / pos_range_ + output_center_
152-
: calibrated / neg_range_ + output_center_;
165+
T output = positive_input ? calibrated / pos_range_ + output_center_
166+
: calibrated / neg_range_ + output_center_;
153167
if (invert_output_) {
154168
output = -output;
155169
}
156-
return output;
170+
return std::clamp(output, output_min_, output_max_);
157171
}
158172

159173
/**
160174
* @brief Unmap a value \p v from the configured output range (centered,
161175
* default [-1,1]) back into the input distribution.
162176
* @param T&v Value from the centered output distribution.
163177
* @return Value within the input distribution.
178+
* @note If `invert_input` is true, then the max/min of the input range both
179+
* map to the output center, which means that unmapping a value at the
180+
* output center is ambiguous. In this case unmap() will return the
181+
* maximum input value.
164182
*/
165183
T unmap(const T &v) const {
166-
T calibrated =
167-
v >= T(0) ? (v - output_center_) * pos_range_ : (v - output_center_) * neg_range_;
184+
T clamped = std::clamp(v, output_min_, output_max_);
185+
T calibrated{0};
168186
if (invert_output_) {
169-
calibrated = -calibrated;
187+
clamped = -clamped;
188+
}
189+
bool positive_output = clamped >= output_center_;
190+
if (positive_output) {
191+
calibrated = (clamped - output_center_) * pos_range_;
192+
} else {
193+
calibrated = (clamped - output_center_) * neg_range_;
170194
}
171-
T clamped = calibrated + center_;
172195
if (invert_input_) {
173-
clamped = calibrated >= T(0) ? maximum_ - calibrated : minimum_ - calibrated;
196+
if (clamped == output_center_) {
197+
// NOTE: we cannot know if the original input value was minimum or maximum
198+
// so we return the maximum value
199+
return maximum_;
200+
}
201+
calibrated =
202+
positive_output ? maximum_ - calibrated + deadband_ : minimum_ - calibrated + deadband_;
203+
} else {
204+
if (clamped == output_center_) {
205+
return center_;
206+
}
207+
calibrated =
208+
positive_output ? calibrated + center_ + deadband_ : calibrated + center_ - deadband_;
174209
}
175-
return std::clamp(clamped, minimum_, maximum_);
210+
return std::clamp(calibrated, minimum_, maximum_);
176211
}
177212

178213
protected:

0 commit comments

Comments
 (0)