92

I am looking for ideas on how to translate one range values to another in Python. I am working on hardware project and am reading data from a sensor that can return a range of values, I am then using that data to drive an actuator that requires a different range of values.

For example lets say that the sensor returns values in the range 1 to 512, and the actuator is driven by values in the range 5 to 10. I would like a function that I can pass a value and the two ranges and get back the value mapped to the second range. If such a function was named translate it could be used like this:

sensor_value = 256
actuator_value = translate(sensor_value, 1, 512, 5, 10)

In this example I would expect the output actuator_value to be 7.5 since the sensor_value is in the middle of the possible input range.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
Tendayi Mawushe
  • 25,562
  • 6
  • 51
  • 57
  • 3
    Thanks for all the answers folks, I accepted Adam Luchjenbroers' answer as it is closely aligned with what I was thinking, without bringing in third party libraries for a relatively simple task. – Tendayi Mawushe Dec 28 '09 at 13:39

10 Answers10

120

One solution would be:

def translate(value, leftMin, leftMax, rightMin, rightMax):
    # Figure out how 'wide' each range is
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin

    # Convert the left range into a 0-1 range (float)
    valueScaled = float(value - leftMin) / float(leftSpan)

    # Convert the 0-1 range into a value in the right range.
    return rightMin + (valueScaled * rightSpan)

You could possibly use algebra to make it more efficient, at the expense of readability.

Adam Luchjenbroers
  • 4,917
  • 2
  • 30
  • 35
  • @Adam thank you for your answer. I had the same problem and it took me some time to find the right solution. Then I wanted to post new question on sf with my solution and ask if it is possible to solve it in more efficient way. Could you please show the solution by using algebra? – Matt Nov 28 '14 at 11:05
  • 5
    Great solution. One recommendation is use 'to' and 'from' in the code instead of 'left' and 'right' which can be confusing. – catalyst294 May 11 '15 at 20:38
  • 1
    @catalyst294 - have to use `from_`, as `from` is a reserved word in Python. – PaulMcG Jul 22 '15 at 20:14
  • Python Zen: "Explicit is better than implicit". I doubt you're going to see much of a speed increase, so might as well keep it pretty! – Spencer Apr 08 '18 at 18:30
117

Using scipy.interpolate.interp1d

You can also use scipy.interpolate package to do such conversions (if you don't mind dependency on SciPy):

>>> from scipy.interpolate import interp1d
>>> m = interp1d([1,512],[5,10])
>>> m(256)
array(7.4951076320939336)

or to convert it back to normal float from 0-rank scipy array:

>>> float(m(256))
7.4951076320939336

You can do also multiple conversions in one command easily:

>>> m([100,200,300])
array([ 5.96868885,  6.94716243,  7.92563601])

As a bonus, you can do non-uniform mappings from one range to another, for intance if you want to map [1,128] to [1,10], [128,256] to [10,90] and [256,512] to [90,100] you can do it like this:

>>> m = interp1d([1,128,256,512],[1,10,90,100])
>>> float(m(400))
95.625

interp1d creates piecewise linear interpolation objects (which are callable just like functions).

Using numpy.interp

As noted by ~unutbu, numpy.interp is also an option (with less dependencies):

>>> from numpy import interp
>>> interp(256,[1,512],[5,10])
7.4951076320939336
sastanin
  • 40,473
  • 13
  • 103
  • 130
  • 37
    You could also use `numpy.interp(256,[1,512],[5,10])`, to reduce the dependency to numpy. – unutbu Dec 28 '09 at 12:54
  • 2
    To convert `array([ 5.96868885, 6.94716243, 7.92563601])` to a list, use `m([100,200,300]).tolist()`. – zanetu Apr 06 '14 at 05:26
  • 1
    Also, `scipy.interpolate.interp1d` can be much slower than `numpy.interp`. (See [this question](http://stackoverflow.com/questions/12240634/what-is-the-best-drop-in-replacement-for-numpy-interp-if-i-want-the-null-interpo).) Benchmark you code first if you are concerned with performance. – zanetu Apr 06 '14 at 05:56
  • For the non uniform interpolattion i want to map numbers between 0 and 2 to -15 and 1 and numbers betwen higher than 2 and 6 to 15 and 1. Is this possible ? I triedinterp1d( [0,2,2.1,6],[-15,1,15,1]) but i get the error: A value in x_new is above the interpolation range. – Varlor Feb 15 '18 at 23:53
  • Furthermore, is it possible also to do interpolation non linear for example exponential or square interpolation? – Varlor Feb 16 '18 at 00:34
  • Does this take care of negative imputs? – Atif Ali Jul 02 '22 at 08:48
30

This would actually be a good case for creating a closure, that is write a function that returns a function. Since you probably have many of these values, there is little value in calculating and recalculating these value spans and factors for every value, nor for that matter, in passing those min/max limits around all the time.

Instead, try this:

def make_interpolater(left_min, left_max, right_min, right_max): 
    # Figure out how 'wide' each range is  
    leftSpan = left_max - left_min  
    rightSpan = right_max - right_min  

    # Compute the scale factor between left and right values 
    scaleFactor = float(rightSpan) / float(leftSpan) 

    # create interpolation function using pre-calculated scaleFactor
    def interp_fn(value):
        return right_min + (value-left_min)*scaleFactor

    return interp_fn

Now you can write your processor as:

# create function for doing interpolation of the desired
# ranges
scaler = make_interpolater(1, 512, 5, 10)

# receive list of raw values from sensor, assign to data_list

# now convert to scaled values using map 
scaled_data = map(scaler, data_list)

# or a list comprehension, if you prefer
scaled_data = [scaler(x) for x in data_list]
PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • I like this answer because it does not clamp the transformed values at the specified min/max, but allows them to go "out of bounds." – Clay Aug 16 '18 at 16:15
14

I was looking for the same thing in python to map angles 0-300deg to raw dynamixel values 0-1023, or 1023-0 depending on the actuator orientations.

I ended up going very simple.

Variables:

x:input value; 
a,b:input range
c,d:output range
y:return value

Function:

def mapFromTo(x,a,b,c,d):
   y=(x-a)/(b-a)*(d-c)+c
   return y

Usage:

dyn111.goal_position=mapFromTo(pos111,0,300,0,1024)
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
James Cann
  • 151
  • 1
  • 3
7
def translate(sensor_val, in_from, in_to, out_from, out_to):
    out_range = out_to - out_from
    in_range = in_to - in_from
    in_val = sensor_val - in_from
    val=(float(in_val)/in_range)*out_range
    out_val = out_from+val
    return out_val
inspectorG4dget
  • 110,290
  • 27
  • 149
  • 241
6

Simple map range function:

def mapRange(value, inMin, inMax, outMin, outMax):
    return outMin + (((value - inMin) / (inMax - inMin)) * (outMax - outMin))
Harisreedhar
  • 173
  • 1
  • 5
4
def maprange(a, b, s):
    (a1, a2), (b1, b2) = a, b
    return  b1 + ((s - a1) * (b2 - b1) / (a2 - a1))


a = [from_lower, from_upper]
b = [to_lower, to_upper]

found at https://rosettacode.org/wiki/Map_range#Python_

  • does not clamp the transformed values to the ranges a or b (it extrapolates)
  • also works when from_lower > from_upper or to_lower > to_upper
Joe
  • 6,758
  • 2
  • 26
  • 47
1

All of the existing answers are under the CC BY-SA license. Here's one that I wrote; to the extent possible under law, I waive all copyright or related and neighboring rights to this code. (Creative Commons CC0 Public Domain Dedication).

def remap(number, from_min, from_max, to_min, to_max):                         
    number_s = number - from_min
    from_max_s = from_max - from_min
    to_max_s = to_max - to_min
    return ((number_s / from_max_s) * to_max_s) + to_min
kj7rrv
  • 11
  • 1
0

You could use a lambda function

translate = lambda a, b, c, d, e: (a - b) * (e - d) / (c - b) + d
sensor_value = 256
translate(sensor_value, 1, 512, 5, 10)
>> 7.495107632093934
Queuebee
  • 651
  • 1
  • 6
  • 24
0

v is value. Maps a-b range to c-d range

def map_range(v, a, b, c, d):
       return (v-a) / (b-a) * (d-c) + c

usage

a = map_range(4, 0, 10, 0, 1)
print(a) # -> 0.4
zoltron
  • 67
  • 6
  • 1
    Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, **can you [edit] your answer to include an explanation of what you're doing** and why you believe it is the best approach? – Jeremy Caney Jun 21 '23 at 01:26