Tuesday, June 9, 2009

Circular number systems

When one deals with angles one must eventually find a way to work around the barrier between 359 degrees and 0 degrees.  For example, if one has an angle at 5 degrees and another at 355 degrees, what is the distance between them?  If one subtracts normally the result will be either 350 or -350, depending on the order one places them in the expression.  This is mathematically correct, producing the difference, but rarely is that what one wants.

The general case is finding out how to rotate an object to a particular destination.  Ideally one should rotate the object such that the shortest amount of time is taken, which means taking the shortest distance.  Rotating 350 degrees is obviously, to human perception, not the shortest distance.  The shortest distance is -10 degrees, of course, but the problem is how to write a program to give that result.

It is easy, if one uses a couple 'if-else' statements.  We start at 5 degrees and need to get to 355, so try the basic equation:

distance = destination - origin

distance = 355 - 5

350

This is, as mentioned earlier not the answer we want, so subtract maximum span of angle values, 360, and we have our answer: -10.  This works as long as the distance crosses over zero and the basic equation results in a positive number.  If it results in a negative number, then we add 360 instead.  If we are not crossing over zero, we add or subtract nothing.  One can always tell if the distance crosses over zero if the result of the basic equation is outside of the range (-180, 180).

Let's turn this into Python code:

def diff (start, end):
    dist = end - start
   
    if dist < -180:
        dist += 360
    if dist > 180:
        dist -= 360
   
  return dist

Simple enough and it works to a degree even when the values are not clamped to (0, 360].  However, I said 'to a degree'.  If the starting angle is 721 and the end is -1, the distance, according to this function is -362.  721 degrees is actually just 1 degree, so the distance should be -2.  To solve for this we should convert all values such that they appear between the standard range of (0, 360].  Therefore the new function is:

def diff (start, end):
    start = start - int (start / 360) * 360
    end = end - int (end / 360) * 360
   
    dist = end - start
   
    if dist < -180:
        dist += 360
    if dist > 180:
        dist -= 360
   
    return dist

This works, perfectly, but it's a rather long function.  For a time I was satisfied with it, but thought that a shorter way was possible.  I presented my problem and solution on the PyGame newsgroup and was greeted with some much better solutions.  Here's one:

def diff (start, end):
    return (end - start + 180) % 360 - 180

I started with a function with an 8-line body of code (not counting whitespace) and got back a function with a single line of code.  There's no conditional statements, either.  It is simply perfect.

This function is easily modified to work with whatever range of values one chooses.  If one works with radians instead of degrees:

def diff (start, end):
    return (end - start + 0.5 * pi) % pi - 0.5 * pi

Or how about a twelve-hour clock?

def diff (start, end):
    return (end - start + 6) % 12 - 6

Perfect!

1 comment:

  1. Bearing in mind that radians go from -pi to pi, I believe the radians function should actually be:
    (end - start + pi) % (pi*2) - pi

    At least, this seems to be working for me, while the one in your post didn't seem to be working.

    ReplyDelete

Followers