-
-
Notifications
You must be signed in to change notification settings - Fork 7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
map function causes huge errors when mapping to small intervals due to integer truncation #10382
Comments
@parapilot And to get proper rounding it needs to add (in_max - in_min) /2 return ((x - in_min) * (out_max - out_min) + (out_min * (in_max - in_min)) + (in_max - in_min) /2) / (in_max - in_min); This will add 3 possible big integers so might overflow even faster. An option is to use floats to minimize over- and underflow return out_min + round( (float(x - in_min) * (out_max - out_min)) / (in_max - in_min) ); If performance is an issue you might have a look at - https://github.com/RobTillaart/FastMap |
A simple fix is to use EDITI didn't realise that the |
As it stands, For instance, the line analogWrite(out_pin, map(analogRead(in_pin), 0, 1024, 0, 256)); achieves a perfectly uniform mapping from the range of Half-open intervals (and, specifically, open on the right side) are not something exotic. They are already present in the API of the Arduino core:
For consistency with those uses, and in order to avoid breaking existing sketches, I think it is better to keep this existing behavior. Avoiding the overflows would still be a good thing, if not too expensive. Instead of fixing the function, I suggest fixing its documentation. Specifically, the provided example: val = map(val, 0, 1023, 0, 255); which gives a less than ideal mapping. |
@edgar-bonet +1 I like the mapping of Fahrenheit to Celsius and vice versa as example
|
@RobTillaart Several comments:
I have a test to demonstrate this last point. You will note that all temperatures from down 212°F to 41°F map correctly, which is where the 'highest in_min' occurs; this demonstrates that my alternative mappings of °F to °C are correct. Lower than that, and only 'exact' mapping work. (BTW I have created my own FastMapInt that fixes this issue; are you interested?): test(T0400map_FToC_OptionsComparison) {
int f, c1, c2, c3;
bool failed = false;
for(f = 212; f >= 0; --f)
{
c1 = map(f, -40, 212, -40, 100);
c2 = map(f, 32, 212, 0, 100);
c3 = map(f, 41, 212, 5, 100);
if(c1 != c2 || c1 != c3){
failed = true;
Serial.print(F("Mismatch at ")); Serial.print(f, DEC); Serial.print(F("°F: C1=")); Serial.print(c1, DEC); Serial.print(F(", C2=")); Serial.print(c2, DEC); Serial.print(F(", C3=")); Serial.println(c3, DEC);
}
}
assertFalse(failed);
} which gives output of (my apologies for terrible formatting; I can't seem to get |
I know of the map problems for years, and seen this discussion more than once. Map works well enough for this purpose, however for serious code one must ask if the errors and impact due to truncating are acceptable or not. E.g a humidity sensor in a weather station with 5% accuracy will be less problematic than a lidar used to prevent collisions. Furthermore a question is if the mapping needed is linear e.g NTC resistor but that is a separate discussion.
Map is a staircase function that lies under a line with a certain slope. (Pos domain story) Worst case it calculates zero points correctly, due to the systematic error. You showed very well in your test.
P/q ==> (p+q/2)/q. Will create its own set of problems but the average error improves quite a bit. And yes negative numbers need to be treated with additional care.
` ``cpp Code ` `` But without the space between the backquotes
|
Wrote a small piece of code to show the staircasing of the map function with CtoF, visualized with the IDE plotter void setup()
{
Serial.begin(115200);
for (float celsius = -20; celsius < 20; celsius += 0.2)
{
Serial.print(celsius * 9.0 / 5.0 + 32.0);
Serial.print("\t");
Serial.print(map(celsius, -40, 100, -40, 212));
Serial.println();
}
}
void loop(){} Visualization of the absolute error shows that map (for this conversion) is
part of the problem here is that the output range is larger than the input range float ma = 0, mi = 0;
void setup()
{
Serial.begin(115200);
for (float celsius = -20; celsius < 40; celsius += 0.2)
{
float f = celsius * 9.0 / 5.0 + 32.0 - map(celsius, -40, 100, -40, 212);
if (f < mi) mi = f;
if (f > ma) ma = f;
Serial.print(f);
Serial.println();
}
Serial.println(mi);
Serial.println(ma);
void loop(){} |
Thank you very much for all your input. After quite a busy time I found some time to look at my original problem. Back when I "solved" it I was quite sure it's a minor change and didn't see all the impact myself. Therefore I didn't mention the context. Looking at the stuff with all this information it took me quite a while to remember how I came to my "fix". First of all I think RobTillaart is right that in general my suggested change is worse regarding integer overflows. (To me this was not really a problem cause I was thinking that the reason for map to use long is to prevent such cases of overflow thinking in the sense that the input values would normally be only 8 or 16 bit values and using long is some kind of explicit upcasting.) Nevertheless I think my problem is explained / addresses by edgar-bonet's comment about the intervals. I tried to use the map function to map the analog reading of a primitive analog joystick. Doing an initial calibration to compesate the zero point offset I tried something like With my change this behavior is changed because the late division shifts truncation to the end of the calculation. Thus I get symmetric behavior around the zero point but to the cost that the uniformity is lost since I get exactly one time the border value compared to bin_size each value inbetween. And with my double mapping around zero even the double amount of zeros. Which is wanted for my usecase but indeed absolutely no general math function. With this said I also think the current implementation of the map function is as correct / general as possible for integer math. But I agree that it could help to explain the behavior better in the documentation. I don't have a really straight forward idea, but I think the case brewmanz mentioned |
Maybe this issue should be merged with these 3 others? |
Thanks @PaulStoffregen. I will close this as a duplicate of arduino/ArduinoCore-API#51 Thanks to everyone for the excellent discussion here I will add a reference from the other issue to make sure it is not missed by interested parties. If you have additional information to share, please feel free to comment on arduino/ArduinoCore-API#51. I don't have permissions in the arduino/ArduinoCore-API repo, so I don't do any maintenance on that issue tracker. So if any of the other issues Paul referenced need to be closed as duplicates then someone else will need to handle that. |
It's unfortunate issues can't be actually merged somehow. |
Regarding the visualizations above, the conversion from float to integer uses truncation rather than proper round off, so the plots show errors which are the sum of map()'s errors and the round-off error of passing a float to an integer input. I would recommend testing with this:
|
I also wish for a true issue merge capability like the one we use all the time on the Arduino forum. As far as I know, the only way to merge on GitHub would be making a reply in the target thread with a copy of each comment in the source thread. We have done something similar to that in cases where we needed to transfer an open issue or PR from one repository to another where there wasn't access to GitHub's issue transfer feature (either prior to the feature's introduction or because the transfer exceeds the limitations imposed on that feature). I considered doing that manual copy for the comments here, but wasn't sure whether it was appropriate or not. If you think it would be best, I'm happy to do it. |
Likewise for the error plotting example above, the large jump in error is due to the program's use of truncation rather than proper round off before map() gets the input. Here's a version which converts from float to int using proper rounding before map().
With this version, the huge jump in error disappears. That wasn't map(). It's the result of improper round-off before map(). Of course, map() still has problems. Anyone can easily see the difference is mostly above zero in this plot. Ideally the difference from ideal due to integer precision should be centered around zero. |
@PaulStoffregen |
I kinda feel like we need an Anakin & Padme meme "So Arduino will fix map()" |
the map function with this implementation
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
while mathematically correct behaves not as expected due to integer math
the implementation should work using it this way (since it compensates for accuray until final division)
return ((x - in_min) * (out_max - out_min) + (out_min * (in_max - in_min))) / (in_max - in_min);
The text was updated successfully, but these errors were encountered: