Go Down

Topic: Distribution evenness of Arduino’s map() function? (Read 5111 times) previous topic - next topic

jj33

I was recently using map() to map a pot input to the range 0,1.  I noticed that I could only get a "1" as output when the pot was turned 100%.  I had expected that the output value would change from 0 to 1 at about 50%.

I spent some time looking at the map() docs at http://www.arduino.cc/en/Reference/Map, both the explanation of integer math and truncated rather than averaged value, plus the actual implementation code, and I now completely understand why I have to have pot cranked to 100% to get a "1" output.

(quick aside in case it's not obvious.  Here's the output from a couple of example maps:

map(x, 0, 1023, 0, 15);
  0   69
  1   68
  2   68
  3   68
  4   68
  5   69
  6   68
  7   68
  8   68
  9   68
10   69
11   68
12   68
13   68
14   68
15    1

map(x, 0, 1023, 0, 1);
  0  1023
  1    1

The output can be properly normalized by adding one to both in_max and out_max (and then wrapping in constrain(), which I should probably do anyway).  Here's what the output distribution looks like for the same application, but with the maxes raised by one:

map(x, 0, 1024, 0, 16);
  0   64
  1   64
  2   64
  3   64
  4   64
  5   64
  6   64
  7   64
  8   64
  9   64
10   64
11   64
12   64
13   64
14   64
15   64

map(x, 0, 1024, 0, 2);
  0  512
  1  512

So, ok, great.  I found an issue in my own code, it matches the textual documentation and implementation of the library function, and I know how to change my expectations and/or code to get what I want.

Here's my question though - should this issue be documented anywhere?  I'm sure I'm not the first person to find this issue because I've found some example sketches where the author did it correctly (and did it in a way where I'm almost positive they did it on purpose), but the vast majority of code you come across on the internet uses the method which will result in an uneven distribution, and I haven't been abe to google up any discussion of the distribution issue.  Even the map() documentation uses an example that will result in an uneven distribution without directly pointing out the issue.

Am I crazy?  I know that for the most part, with analog in and out, this isn't a big deal, but it still seems really odd to me that I can't find any discussion of the issue.  I don't have a ton of experience with arduino programming so I may be missing something obvious, would love some more experienced eyes on this.

Thanks!

robtillaart


This point is discusssed a few times in the old forum. Your analysis of the integer math etc is quite correct.

The formula used is derived from a floating point version which works great. Keeping the math in integer domain keeps it fast, but indeed with some quirks.

Rob

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Coding Badly

#2
Sep 13, 2011, 07:04 am Last Edit: Sep 13, 2011, 07:14 am by Coding Badly Reason: 1

Welcome to the forum.

Here's my question though - should this issue be documented anywhere?


Technically it is.  As you know, this is from the map documentation...
Quote
Fractional remainders are truncated, and are not rounded or averaged.


The part that's missing is a description of the implications of truncating.

Quote
I'm sure I'm not the first person to find this issue...


Nope...
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1257716581

I believe there have been other discussions of map and I know there are several variations.

Quote
Am I crazy?


Difficult to say because we just met.  Oh, you mean, "am I crazy for wanting a map function that provides an even distribution?"  Nope.  Haven't found anyone who actually wants map to work the way it does.

Quote
I know that for the most part, with analog in and out, this isn't a big deal


Ah, but it can be a big deal.  For example: throw a PID loop in there and it can become impossible to tune.

Quote
but it still seems really odd to me that I can't find any discussion of the issue


Include my moniker in the search.  I have a tendency to get involved in those types of discussions.
http://www.google.com/?q=%22coding+badly%22+map+function+truncate+round+site:arduino.cc

jj33

Thank you to robtillaart and Coding Badly for responding, and especially for the link to the previous discussion.  I couldn't find the right search terms to get to it.

With respect to the linked thread, I very much don't think that the underlying code should be changed.  There are too many projects in the wild that use the existing functionality.  I also agree that, once you know what you're looking for, the docs for map() are correct.  They're just not as useful as they could be if they explained the practical implementation of the value truncation.

Is there any reason that couldn't be clarified?  I see that it's a wiki but I don't feel right modifying that page without at least one person concurring.  How about this as an addition to http://www.arduino.cc/en/Reference/Map?

Discussion

Because the underlying math is done in integer arithmetic, users can sometimes be caught off guard when the output of the map() function is not evenly distributed over the input range.  As an extreme example, consider the following map() call:

Code: [Select]
map(x, 0, 1023, 0, 1);

A user of map() might assume that input values 0..511 would result in an output of 0, and 512..1023 would result in 1.  However, because of the truncation that happens in the function, the outputs actually look like {0..1022->0, 1023->1}.

If a user of the map() function wants a more even distribution of output values, this can be done by raising max_in and max_out by one.  For instance, the previous example can be rewritten like this:

Code: [Select]
map(x, 0, 1024, 0, 2);

This function call now results in a perfectly even distribution {0..511->0, 512..1023->1}. 

See also the discussion of constrain() for protecting against unexpected input values.

Coding Badly

Quote
Is there any reason that couldn't be clarified?


Insufficient privileges to perform action.

Once this change has been made to the Tutorial...
http://arduino.cc/forum/index.php/topic,71954.0.html

We'll start beating up the Gatekeeper to make your suggested change.


nayana

Be positive and keep smiling

IDoSoLikeGreenEggsAndHam

#7
Dec 05, 2011, 12:15 am Last Edit: Dec 05, 2011, 11:38 am by IDoSoLikeGreenEggsAndHam Reason: 1
Thanks for reviving this topic.
It seems to me that the documentation should:
a) steer users to a new mapping function which handles the +/-1 adjustment to the endpoints and considers using rounding.
b) warn that the function is based on hybrid math concepts which eventually break down for smaller ranges.  It should probably warn that its 10-bit to 8-bit mapping example is not optimal, and could drive the message home by showing the problem of mapping to a single bit.
c) warn that the function does not continue the regular stair-step pattern when crossing the in_min boundary.
d) clean up official arduino sample code to use proper mapping techniques


The problem with uneven partitioning stems from miscalculation of the slope of the mapping, not so much truncation. The mapping formula can roughly be described as being like y=trunc(mx)+b where the slope m = (out_max - out_min) / (in_max - in_min).  Attempts to map 8 points {0,1,2,3,4,5,6,7} onto  4 points {0,1,2,3} using map(value,0,7,0,3) are screwy because the function roughly parallels a line of slope m=3/7 instead of a slope m=4/8.  3/7 is the wrong compression for this mapping regardless of rounding or truncating.  By adjusting the out_max & in_max values, the user is adjusting the slope, not the truncation.   Extrapolating outside the range may be even more reliant on the correct slope.  In this example, the slope 4/8 extrapolates to include the points (8,4), (64,16) as may be expected.  Introducing rounding to the formula can offer some smoothness to the partitioning, but would not generally correct for the miscalculated slope.  Maybe wrong and miscalculated are strong words--there may be applications where the slope of 3/7 is actually desirable.

Speaking of extrapolating outside of the range, truncation does cause a break in the staircase pattern when crossing the in_min boundary. That tread is nearly twice as wide as the other treads in the pattern.  That is, the out_min value is generally represented more than other step values.  Rounding instead of truncation might offer a niceness to that situation, so long as it is not done at the expense of even tread width within the range.


Msquare


a) steer users to a new mapping function which handles the +/-1 adjustment to the endpoints and considers using rounding.
b) warn that the function is based on hybrid math concepts which eventually break down for smaller ranges.  It should probably warn that its 10-bit to 8-bit mapping example is not optimal, and could drive the message home by showing the problem of mapping to a single bit.
c) warn that the function does not continue the regular stair-step pattern when crossing the in_min boundary.
d) clean up official arduino sample code to use proper mapping techniques
As you may have noticed, there are LOTS of "facts" about every function which is not mentioned, not just the map() function. Someone has decided that they do not want to scare newbeginners by presenting too much on every function. As such I think that is reasonable. What I do miss is a link on every function to the deeper details when they are needed. Some versions ago there was a content page with simple library function and another page including the more complex library functions. There is probably a reason why they dropped the two-stage approach.

IDoSoLikeGreenEggsAndHam

I think my comments are justifiable even in light of keeping special-case comments minimal and non-confusing:
a) Creating a separate, truly integer-based map function, say categorize(), would alleviate the need for complex documentation. Otherwise, suppression of documentation for discrete math would be suspicious.
b) Minimizing documentation would surely call for removal of the misleading 10-to-8 bit example--it is needlessly incorrect for discrete math.
c) If we are concerned about minimizing special "facts", then the comment regarding using values out of range may be a candidate for removal.  It has a pitfall in that the pattern changes near in_min, so why promote it without caution? On the other hand, a proper introduction to the purpose of this function can clarify a great deal of misconceptions.
d) It's inescapable that the switchCase example sketch needs to be corrected.  It's wrong in the same way as jj33 introduced in this thread.

Go Up