Mapping angles

wingnut

Donator
Donator
Joined
May 10, 2013
Messages
129
Reaction score
0
Points
16
For a free-time programming effort I want to map relative bearings from 0 to 360 degrees to position strings like "front", "right", "back", "left" as illustrated in the attached image.

I'm having problems with handling the "front" sector due to the transition from 359 to 0 degrees. Checking the interval in the fashion of
Code:
IF relative-bearing >= 315 AND relative-bearing <= 45
surely does not work.

I can handle this case with a special interval from 315 to 360 degrees and from 0 to 45 degrees.
But I'm wondering if there is any generic - and not too complicated - way to handle all of the four situations reliably (the sectors are pretty arbitrary for this example, I might also use more than four parts of the circle, e. g. for "Nort-north-west" etc.)?
 

Attachments

  • attachment.png
    attachment.png
    10 KB · Views: 23

Ripley

Tutorial translator
Donator
Joined
Sep 12, 2010
Messages
3,133
Reaction score
407
Points
123
Location
Rome
Website
www.tuttovola.org
Pseudocode:
Code:
If
   (angle >45 and angle <=135) right
elseif
   (angle >135 and angle <=225) down
elseif
   (angle >225 and angle <=315) left
else front
endif

Or, in Excel (cell M9 has the value):
Code:
=if(and(M9>45;M9<=135);"right";if(and(M9>135;M9<=225);"down";if(and(M9>225;M9<=315);"left";"front")))

No?
Of course there will be one million more elegant ways to do it.
 

Hielor

Defender of Truth
Donator
Beta Tester
Joined
May 30, 2008
Messages
5,580
Reaction score
2
Points
0
For the "stupid programmer tricks" version, assuming that angle is an unsigned value:

Subtract 45 from the angle. Anything less than 45 has now wrapped to be a very large number.

and then let [0-90) be "right", [90-180) be "back", [180-270) be "left", and anything else be "front"

:lol:
 

francisdrake

Addon Developer
Addon Developer
Joined
Mar 23, 2008
Messages
1,086
Reaction score
903
Points
128
Website
francisdrakex.deviantart.com
Depending on how the angle-value is created it could be negative. To be on the safe side I use to check this. If true, I add 360° (or the 2 pi when in radians).

while (angle < 0) angle+=360;

After that, the checking routine shall fall through as quickly as possible.
I think Ripley's if - elseif code is pretty fast and hard to beat speed-wise.

The only thing I would add if the angle is in degrees I would do a cast to integer before the comparision starts, to avoid floating point comparision.
 

wingnut

Donator
Donator
Joined
May 10, 2013
Messages
129
Reaction score
0
Points
16
Yes, Ripley's if - elseif is a nice solution of which I had not thought.

I forgot to mention in the first post, that I cannot necessarily check all boundaries at once but have to iterate through a collection which has the left and right boundaries defined for each element and the circle is not always divided into equal parts. But there are no overlaps.
 

Attachments

  • attachment2.png
    attachment2.png
    15 KB · Views: 10

Hlynkacg

Aspiring rocket scientist
Addon Developer
Tutorial Publisher
Donator
Joined
Dec 27, 2010
Messages
1,870
Reaction score
3
Points
0
Location
San Diego
Simplest way...

Code:
// Assuming angle is a value between 0 - 360... 

if (angle > 345 || angle < 45) return "front";
else if (angle > 45 && angle < 135) return "right";
else if (angle < 345 && angle > 225) return "left";
else return "back";
 
Last edited:

francisdrake

Addon Developer
Addon Developer
Joined
Mar 23, 2008
Messages
1,086
Reaction score
903
Points
128
Website
francisdrakex.deviantart.com
// Another idea to shorten the code:
// Working backwards saves the second comparision,
// 'return' jumps out of the function.

which_dir(angle) {
if (angle > 345) return "front";
if (angle > 225) return "left";
if (angle > 135) return "back";
if (angle > 45) return "right";
return "front"; // this is for the 0-45° angle
}
 

N_Molson

Addon Developer
Addon Developer
Donator
Joined
Mar 5, 2010
Messages
9,290
Reaction score
3,258
Points
203
Location
Toulouse
Thanks, quite an useful post ! :idea:
 

Urwumpe

Not funny anymore
Addon Developer
Donator
Joined
Feb 6, 2008
Messages
37,628
Reaction score
2,345
Points
203
Location
Wolfsburg
Preferred Pronouns
Sire
Personally, I dislike such solutions like francisdrake's from various testability reasons (If you miss something, the function reacts chaotic and you will not find a precise symptom-cause relationship, especially for bigger functions).

The Hlynkacg solution is my personal favorite to minimal spec. Should there be more parts of a circle, you can of course try it with a list of start-, end-labels and text, the angles are defined between 0 and 405°. If the input angle is smaller than 45°, you add 360° to the angle for the end-angle check, so that you never need to work with negative numbers at all. And then you simply check until you have the first fit or nothing...
 
Last edited:

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
696
Reaction score
259
Points
78
Location
This place called "home".
In the hunt for the shortest code, here is my contribution:

Code:
name [4] = {'front', 'right', 'back', 'left'};
direction = name [floor(4 * mod(angle + 45, 360) / 360)];

It should work for any angle, both negative and positive, and angles of more than one rotation (e.g. -720 degrees would return 'front').
 

wingnut

Donator
Donator
Joined
May 10, 2013
Messages
129
Reaction score
0
Points
16
Thank you all for your interest and contributions.

The angle between the two points targetPt and centerPt is calculated with this code which should produce values between 0 and 360
Code:
function calcAngle() {
    theta = atan2(targetPt.y - centerPt.y, targetPt.x - centerPt.x);
    theta = theta + PI/2.0;
    angle = toDegrees(theta);
    if (angle < 0) {
        angle = angle + 360;
    }
    return angle;
}
As I indicated before, the initial problem changed a bit so that all of the intervals are not known at once.
I want to check to which relative bearing interval the angle calculated above belongs for the centerPt. The arcs for the centerPt could are defined with left and right borders, for example:
Code:
class Arc {
    double left;
    double right;
    // omit any constructors due to laziness
}

class CenterPt {
    Arc[] arcs = {
        new Arc(345, 60),
        new Arc(60, 180),
        new Arc(180, 270),
        new Arc(270, 345)
    };
}

Then, to check to which of the defined arcs a certain relative bearing belongs I would do:

Code:
bearing = calcAngle(anyAngle);

forEach (CenterPt.arcs) {
    if(arcs.left <= bearing && arcs.right > bearing) {
        // found the arc containing the bearing
        break;

    } else if(arcs.left > arcs.right) {

        if(arcs.left <= bearing || arcs.right > bearing) {

               // found the arc which contains the bearing and spans from 359 to 1        
              break;
        }
    }
}

Would this work?
Edit: a quick unit test indicates that this works, I'm not sure if I missed something though.
 
Last edited:

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
696
Reaction score
259
Points
78
Location
This place called "home".
Time results

Out of interest, I ran through the functions by Ripley, Hielor, Hlynkacg, francisdrake and myself three times and checked the time used by each function while using 3.6 million + 1 different angles between 0 and 360 degrees in MATLAB (every 0.0001 degree).

I copied each code and translated it to MATLAB. Note that Hlynkacg and francisdrake had typos in their code (I fixed them), both assuming the boundrary between left and front was at 345 degrees (it is at 315 degrees), and Hlynkacg's code also ignored values at exactly 315, 45, 135 and 225 degrees, and therefore returning 'back' for those four angles.

I did not discover francisdrake's error until after I had recorded the time, but I did another run afterwards, and the results were comparable.

Here are the results (all numbers are in seconds):
User | First round | Second round | Third round | Average
Ripley | 20.4867 | 20.1066 | 19.9343 | 20.1759
Hielor | 20.4928 | 20.3260 | 20.3308 | 20.3832
Hlynkacg | 34.5959 | 34.8855 | 34.3873 | 34.6229
francisdrake | 34.7324 | 35.0789 | 34.1148 | 34.6420
asbjos | 46.5920 | 46.4698 | 43.8758 | 45.6459

As we can see from the results, Ripley and Hielor had practically the same code (Hielor subtracted 45 from the angle), and therefore the same time, with Hielor only being 1.03 % slower than Ripley.
Hlynkacg and francisdrake both got close to 35 seconds, even though Hlynkacg had almost the same code as Ripley. It seems like the difference is in Hlynkacg using 'return' in each if-else-statement. I ran Hlynkacg's code afterwards without 'return'-lines, and the time then went down to 20.2812 s, just as Ripley and Hielor. I then inserted the 'return'-lines again, and the time went up to normal again.
My own code was the loser in this test, using an average of 46 seconds, more than double the time of Ripley's and Hielor's code. I suspect the fault is in the array of names. I removed the array, so that the function would return 1 (0 in C++) for front, 2 for right, etc., and the time then dropped down to only 6.0934 seconds (a third of Ripley's and Hielor's). But this is unfair, as the code only needs to return a number, and not a word. I rewrote Ripley's code so that it would return 1 for front, 2 for right, etc., and then the time dropped to only 4.8464 seconds.

So it really seems like the simplest is the fastest (naturally), both for humans and computers.


For anyone interested, here is the code in MATLAB:
runAll.m (the script which ran each function 3 600 001 times):
Code:
% http://orbiter-forum.com/showthread.php?p=504866

% Run for angles between 0 and 360 degrees, checking every 10^(-4)
% [= 0.0001] angles, to get more reliable results.

% 'tic' starts (or resets if it's used multiple times) a timer, 'toc'
% returns the current timer value.
% 'clear' removes all values in memory.
% This code assumes that all the functions are correct. It does not check
% if the output value of each function is the correct answer.
% Every for-loop uses a different variable to make sure that no loops
% benefit from previous runs.
% The program was closed and reopened for every run.

value = 0;

% Ripley:
clear
tic
for i = 0 : 10^(-4) : 360
    value = Ripley (i);
end
disp(toc);
disp('^ Ripley');
value = 0;
disp('  ');

% Hielor:
clear
tic
for j = 0 : 10^(-4) : 360
    value = Hielor (j);
end
disp(toc);
disp('^ Hielor');
value = 0;
disp('  ');

% Hlynkacg:
clear
tic
for k = 0 : 10^(-4) : 360
    value = Hlynkacg (k);
end
disp(toc);
disp('^ Hlynkacg');
value = 0;
disp('  ');

% francisdrake:
clear
tic
for l = 0 : 10^(-4) : 360
    value = francisdrake (l);
end
disp(toc);
disp('^ francisdrake');
value = 0;
disp('  ');

% asbjos
clear
tic
for m = 0 : 10^(-4) : 360
    value = asbjos (m);
end
disp(toc);
disp('^ asbjos');
value = 0;
disp('  ');

% end of file

Ripley.m (Ripley's function, converted to MATLAB):
Code:
function direction = Ripley (angle)
    % http://orbiter-forum.com/showthread.php?p=504787&postcount=2
    if angle > 45 && angle <= 135
        direction = 'right';
    elseif angle > 135 && angle <= 225
        direction = 'down';
    elseif angle > 225 && angle <= 315
        direction = 'left';
    else
        direction = 'front';
    end
end

Hielor.m (Hielor's function, converted to MATLAB):
Code:
function direction = Hielor (angle)
    % http://orbiter-forum.com/showthread.php?p=504789&postcount=4
    degree = angle - 45;
    
    if degree >= 0 && degree < 90
        direction = 'right';
    elseif degree >= 90 && degree < 180
        direction = 'back';
    elseif degree >= 180 && degree < 270
        direction = 'left';
    else
        direction = 'front';
    end
end

Hlynkacg.m (Hlynkacg's function, converted to MATLAB):
Code:
function direction = Hlynkacg (angle)
    % http://orbiter-forum.com/showthread.php?p=504800&postcount=7
    
    % Hlyankacg did a mistake, by assuming the boundrary between left and
    % front was at 345, while it is at 315 degrees.
    % Hlyankacg's code also did not think about values at exactly 45, 135,
    % 225 or 315 degrees, which would all return 'back'. The correct code
    % is to check if an angle is '<=', not '<'

    if angle >= 315 || angle < 45
        direction = 'front';
        return
    elseif angle >= 45 && angle < 135
        direction = 'right';
        return
    elseif angle <= 315 && angle > 225
        direction = 'left';
        return
    else
        direction = 'back';
        return
    end
end

francisdrake.m (francisdrake's function, converted to MATLAB):
Code:
function direction = francisdrake (angle)
    % http://orbiter-forum.com/showthread.php?p=504844&postcount=8
    
    % francisdrake did a mistake, by assuming the boundrary between left and
    % front was at 345, while it is at 315 degrees.

    if angle > 315
        direction = 'front';
        return
    end
    if angle > 225
        direction = 'left';
        return
    end
    if angle > 135
        direction = 'back';
        return
    end
    if angle > 45
        direction = 'right';
        return
    end
    direction = 'front';
    return
end

asbjos.m (my function, converted to MATLAB):
Code:
function direction = asbjos (angle)
    % http://orbiter-forum.com/showthread.php?p=504866&postcount=11

    name = {'front', 'right', 'back', 'left'};
    direction = name{ floor(4 * mod(angle + 45, 360) / 360) + 1 }; % '+ 1' because MATLAB starts counting from 1, not 0
end
 
Last edited:

francisdrake

Addon Developer
Addon Developer
Joined
Mar 23, 2008
Messages
1,086
Reaction score
903
Points
128
Website
francisdrakex.deviantart.com
Thanks for the detailed analysis! The result turned out different to what I had expected. I guessed asbjos code being the shortest would be among the fastest, but it is really the other way round. Probably the math involved (division) slows it down.

It still puzzles me why the else-if code with two comparisons and a return is faster than a single comparision and a return.
 

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
I'd like to join the fun with my own entry:
Code:
    if angle < 135
        if angle < 45
            direction = up;
        else
            direction = right;
        end
    elseif angle >= 225
        if angle >= 315
            direction = up;
        else
            direction = left;
        end
    else
        direction = down;
    end
The idea is to minimize the number of comparisons by arranging them hierarchically, i.e. effectively replacing the sequential search with a binary one (although for such a small number of bins, the effect won't be very significant).

I did the timing as well, based on asbjos' code, but I made a few small adjustments:
  • I added the individual algorithms as inline codes, to avoid function calls which will make a substantial part of the runtime and might skew the results. This was trivial for all codes, but on francisdrake's algorithm required substituting continues for the returns.
  • All codes compute numerical values to avoid passing strings.
  • I am using randomized angles rather than sorted ones to prevent matlab from doing anything clever.
  • I'm summing the results, to prevent matlab from optimizing the loops. This also provides a handy checksum to make sure all algorithms do the same thing.
Code:
function test_timing

% Ripley:
clear
rand('state',0);
sum = 0;
tic
for i=0:10000000
    angle = rand*360;
    
    if angle > 45 && angle <= 135
        direction = 1;
    elseif angle > 135 && angle <= 225
        direction = 2;
    elseif angle > 225 && angle <= 315
        direction = 3;
    else
        direction = 0;
    end
    
    sum = sum+direction;
end
disp(toc);
disp(['^ Ripley, sum=' num2str(sum)]);
disp('  ');

% Hielor:
clear
rand('state',0);
sum = 0;
tic
for i=0:10000000
    angle = rand*360;
    
    degree = angle - 45;
    if degree >= 0 && degree < 90
        direction = 1;
    elseif degree >= 90 && degree < 180
        direction = 2;
    elseif degree >= 180 && degree < 270
        direction = 3;
    else
        direction = 0;
    end

    sum = sum+direction;
end
disp(toc);
disp(['^ Hielor, sum=' num2str(sum)]);
disp('  ');

% Hlynkacg:
clear
rand('state',0);
sum = 0;
tic
for i=0:10000000
    angle = rand*360;

    if angle >= 315 || angle < 45
        direction = 0;
    elseif angle >= 45 && angle < 135
        direction = 1;
    elseif angle < 315 && angle >= 225
        direction = 3;
    else
        direction = 2;
    end

    sum = sum+direction;
end
disp(toc);
disp(['^ Hlynkacg, sum=' num2str(sum)]);
disp('  ');

% francisdrake:
clear
rand('state',0);
sum = 0;
direction = 0;
tic
for i=0:10000000
    sum = sum+direction;
    angle = rand*360;

    if angle > 315
        direction = 0;
        continue;
    end
    if angle > 225
        direction = 3;
        continue;
    end
    if angle > 135
        direction = 2;
        continue;
    end
    if angle > 45
        direction = 1;
        continue;
    end
    direction = 0;

end
sum = sum+direction;
disp(toc);
disp(['^ francisdrake, sum=' num2str(sum)]);
disp('  ');

% asbjos
clear
rand('state',0);
sum = 0;
tic
for i=0:10000000
    angle = rand*360;

    direction = floor(4 * mod(angle + 45, 360) / 360);

    sum = sum+direction;
end
disp(toc);
disp(['^ asbjos, sum=' num2str(sum)]);
disp('  ');

% martins
clear
rand('state',0);
sum = 0;
tic
for i=0:10000000
    angle = rand*360;

    if angle < 135
        if angle < 45
            direction = 0;
        else
            direction = 1;
        end
    elseif angle >= 225
        if angle >= 315
            direction = 0;
        else
            direction = 3;
        end
    else
        direction = 2;
    end

    sum = sum+direction;
end
disp(toc);
disp(['^ martins, sum=' num2str(sum)]);
disp('  ');
And here are my timing results (Windows, Matlab 2006):
Code:
    0.4219

^ Ripley, sum=14997892
  
    0.4047

^ Hielor, sum=14997892
  
    0.4224

^ Hlynkacg, sum=14997892
  
    1.7262

^ francisdrake, sum=14997892
  
    0.7017

^ asbjos, sum=14997892
  
    0.3767

^ martins, sum=14997892
Unfortunately, running on a different platform (Linux, Matlab 2012b), gives less conclusive results, with my solution no longer substantially faster:hmm:

Even weirder, if I remove the first line of the code, and run it as a script rather than a function, all results are slower by about a factor 30 for no obvious reason I can think of. Maybe matlab isn't such a good testbed for algorithm profiling.
 

Hielor

Defender of Truth
Donator
Beta Tester
Joined
May 30, 2008
Messages
5,580
Reaction score
2
Points
0
Oh, well, if we're doing it as a competition, I'll present the following, which uses the same 0->3 mapping of bearings that martins established:
Code:
degree = (angle + 45) % 360;
direction = (int)(degree) / 90;
By again aligning the bearings along the axes, and then using integer division of the angle by 90, the angles sort themselves into the buckets for us. :)

If the integer-division thing isn't available, floor(degree / 90) should work too.
 
Last edited:

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
Efficient in C++, but less so in Matlab, where it's essentially the same as asbjos' solution (minus the redundant multiplication by 4):
Code:
    direction = floor(mod(angle+45,360)/90);
For which I get a runtime of 0.6682 in the same environment as the other timings I posted. floor and mod are relatively expensive function calls in Matlab.

Somebody else will have to do the timings in C++ ...
 

Hlynkacg

Aspiring rocket scientist
Addon Developer
Tutorial Publisher
Donator
Joined
Dec 27, 2010
Messages
1,870
Reaction score
3
Points
0
Location
San Diego
% Hlyankacg did a mistake, by assuming the boundrary between left and
% front was at 345, while it is at 315 degrees.
% Hlyankacg's code also did not think about values at exactly 45, 135,
% 225 or 315 degrees, which would all return 'back'. The correct code
% is to check if an angle is '<=', not '<'

That's what I get for rattling off a 30 second answer from my phone rather than using my laptop AKA "working" platform. :blackeye:

Proper solution would use <= and >= in the initial "front" comparison and leave remainder as is.
 
Last edited:
Top