Trondh
Trondh

Reputation: 3341

Calculate difference between two angles

I realize this is probaby the most noob question of all times, but my mind is completely stuck.

I have two angles, which represent the compass direction of two units, lets say

unit1: 90.0

unit2: 45.0

If these differ more than 20 degrees, unit2 needs to take unit1's direction, with +/-20 degrees, so that

unit1:90.0

unit2:70.0

I can figure out the difference in degrees between these two (signed) using

angle = 180 - abs(abs(unit1 - unit2) - 180)

but I need to know whether to adjust by +20 or -20.

For example, for this set:

unit1:270

unit2:350

unit2 needs to become 290 (adding 20 degrees to unit1)

I'm pretty sure there's probably a single python function which does this for me, but I am completely stuck and can't remember any of the math I learned 20 years ago.

I completely expect someone to make a fool out of me, but I would appreciate it anyways.

Upvotes: 1

Views: 11220

Answers (4)

JMaszi
JMaszi

Reputation: 1

I know this is an old post but just wanted to share this nice little one-liner for comparing angles. Implementation uses numpy and leverages a bit of circle math. Only issue you might find is that it will return False for an absolute edge. e.g. unit1 = 90 and unit2 = 70. But will return True if unit2 were to = 70.01.

import math
import numpy

if math.cos(np.deg2rad(unit2) - np.deg2rad(unit1)) >= math.cos(np.deg2rad(20)):
    unit2=unit1

Upvotes: 0

Reti43
Reti43

Reputation: 9796

Conceptually, you are looking for this pseudocode:

if angle_difference > 20:
    if unit1 > unit2:
        unit2 = unit1 - 20
    else:
        unit2 = unit1 + 20

That is, if, for example, unit1 is larger, then unit2 will increase until it's 20 less than unit1. However, there is a trick I missed initially. If the angle difference is greater than 180, their complement is less than 180, so it's shorter for unit2 to approach unit1 from the other direction and the sign calculations switch.

def adjust_angle(a, b):
    angle = abs(a - b)
    if 340 > angle > 20:
        adjustment = -20 if angle < 180 else 20
        b = (a + adjustment if a > b else a - adjustment) % 360
    return a, b

A few points to explain here. In the same way that if the angle difference is less than 20 you don't have to do anything, so is the case for greater than 340 (360-20).

The snippet

if 340 > angle > 20:
    adjustment = -20 if angle < 180 else 20
    b = (a + adjustment if a > b else a - adjustment) % 360

is equivalent in logic to the following

if 340 > angle > 20:
    if angle < 180:
        b = (a - 20 if a > b else a + 20) % 360
    else:
        b = (a + 20 if a > b else a - 20) % 360

By using the adjustment variable, we simply eliminate code replication.

Finally, the modulo operation is used because sometimes in pure calculations unit2 may exceed the value 360. For example, for unit1 = 50 and unit2 = 340, unit2 must increase by 50, effectively becoming 390. But since we're restricted from 0 to 360 degrees, the result will become 30.

Upvotes: 2

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

The current way you are calculating the angle between the two directions will fall apart if the two directions are more then 360 apart:

unit1 = 360 * 4
unit2 = 50
angle = 180 - abs(abs(unit1 - unit2) - 180)
print(angle) #outputs -1050

I would highly recommend using modulus % instead to calculate the angle:

#this will calculate the angle so that 0<=angle<360
angle = (unit2 - unit1) % 360

#this alone will give you the angle such that:
assert 0<= angle <360
assert (unit1 + angle) %360 == unit2 %360

although it would be easier to work with values between -180 and 180 instead of 0 to 360, so you can reduce the values above 180 into the negatives like so:

angle = (unit2 - unit1) % 360
if angle>180:
    angle -=360

# or using @PM2Ring suggestion in comments
angle = 180 - (180 - unit2 + unit1) % 360

#now these two statements will always be True
assert -180< angle<=180
assert (unit1 + angle) %360 == unit2 %360

then you can just check for values over 20, or less then -20:

if angle>20:
    unit2 = unit1 + 20
elif angle<-20:
    unit2 = unit1 - 20
#else leave unit2 unchanged

edit: If you don't mind working with angles between 0 and 360 you can still do equivalent calculations using 340 as cutoff:

if 20<angle<=180:
    unit2 = unit1 + 20
elif angle<340: #elif angle in range(181,340):
    unit2 = unit1 - 20
#else: assert angle in range(21) or angle in range(340,360) #no change

This and the original post use 3 checks to accomplish the task but this method doesn't have a possible extra step of reducing angle so this will execute unnoticeably faster but will probably be more confusing to work with.

Upvotes: 2

Jamtot
Jamtot

Reputation: 129

I can see why you got stuck on that. The other solution didn't really solve for cases like unit1 being at 30 degrees and unit2 being at 340, where it would rotate through the 360 to 0. The code can do with some cleaning up, but I hope this helps somewhat.

import math

# calculate and return the distance unit2 
# needs to move to reach unit1
def get_distance(unit1, unit2):
    phi = abs(unit2-unit1) % 360
    sign = 1
    # used to calculate sign
    if not ((unit1-unit2 >= 0 and unit1-unit2 <= 180) or (
            unit1-unit2 <= -180 and unit1-unit2 >= -360)):
        sign = -1
    if phi > 180:
        result = 360-phi
    else:
        result = phi

    return result*sign

# distance unit2 needs to move to reach unit1
print get_distance(90,45)  # output 45 
print get_distance(270, 350) # output -80 
print get_distance(350, 30) # output -40 (unit2 moves -40 degrees)   
print get_distance(30, 350) # output 40  

unit1 = 30
unit2 = 350
# then calculate distance to move by taking 20 from distance
distance_to_move = (get_distance(unit1, unit2) - 20)

print (unit2 + distance_to_move)%360 # new position 10

I've changed it up a bit to work for more than 360 degrees as pointed out by Tadhg. The variance of 20 degrees is a bit less hard-coded too.

import math

# calculate and return the distance unit2 
# needs to move to reach unit1
def get_distance(unit1, unit2):
    phi = (unit2-unit1) % 360
    sign = -1
    # used to calculate sign
    if not ((phi >= 0 and phi <= 180) or (
            phi <= -180 and phi >= -360)):
        sign = 1
    if phi > 180:
        result = 360-phi
    else:
        result = phi
    return (result*sign), sign

def get_sign(unit1, unit2):
    phi = (unit2-unit1) % 360
    sign = 1
    if ((phi >= 0 and phi <= 180) or (
            phi <= -180 and phi >= -360)):
        sign = -1
    return sign

def new_position(unit1, unit2, variance = 20):
    distance_to_move, sign = get_distance(unit1, unit2)
    variance*=sign
    # %360 to keep it within the circle
    return (unit2+distance_to_move-variance)%360

# distance unit2 needs to move to reach unit1
assert (get_distance(90,45) == (45, 1))
assert (get_distance(270, 350) == (-80, -1))
assert (get_distance(350, 30) == (-40, -1))   
assert (get_distance(30, 350) == (40, 1))
assert (get_distance(50, 360*4) == (50, 1))
assert (get_distance(360*4, 50) == (-50, -1))
print "----------"
assert (new_position(90,45) == 70)
assert (new_position(270, 350) == 290) 
assert (new_position(350, 30) == 10)
assert (new_position(30, 350) == 10)
assert (new_position(50, 360*4) == 30)
assert (new_position(360*4, 50) == 20)

Upvotes: 6

Related Questions