atan2f vs fmodf vs just plain subtraction

Martin

I have a problem with a piece of code that I wrote to wrap an angle around during an integration and is part of a small simulation that I'm working on. So basically the idea is to prevent the angle from growing large by making sure that it always has a sane value. I have tried three different approaches that I would expect to give the same results. And most of the time they do. But the first two give artifacts around the point where angle value wraps around. When I then generate a waveform from the angle value I get undesirable results because of these precision errors.

So the first approach is like this (limit angle to -8PI +8PI range):

self->state.angle = atan2f(sinf(angle / 8), cosf(angle / 8)) * 8;

This creates artifact that looks like this: enter image description here

Second approach of:

self->state.angle = fmodf(angle, (float)(2.f * M_PI * 8))

Creates the same result: enter image description here

However if I just do it like this:

float limit = (8 * 2 * M_PI); 
if(angle > limit) angle -= limit;           
if(angle < 0) angle += limit;               
self->state.angle = a;

Then it works as expected without any artifacts: enter image description here

So what am I missing here? Why do the other two approaches create precision error? I would expect all of them to generate the same result (I know that ranges of the angle are different but when the angle is passed further into a sin function I would expect the result to be the same).

Edit: small test

// g++ -o test test.cc -lm && ./test
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <stdint.h>

int main(int argc, char **argv){
    float a1 = 0;
    float a2 = 0;
    float a3 = 0;
    float dt = 1.f / 7500.f;

    for(float t = -4.f * M_PI; t < (4.f * M_PI); t+=dt){
        a1 += dt;
        a2 += dt;
        a3 += dt;

        float b1 = a1;
        if(b1 > 2.f * M_PI) b1 -= 2.f * M_PI;
        if(b1 < 0.f) b1 += 2.f * M_PI;
        float b2 = atan2f(sinf(a2), cosf(a2));
        float b3 = fmodf(a3, 2 * M_PI);

        float x1 = sinf(b1);
        float x2 = sinf(b2);
        float x3 = sinf(b3);

        if((x1 * x2 * x3) > 1e-9){
            printf("%f: x[%f %f %f],\tx1-x2:%f x1-x3:%f x2-x3:%f]\n", t, x1, x2, x3, (x1 - x2) * 1e9, (x1 - x3) * 1e9, (x2 - x3) * 1e9);
        }
    }

    return 0;
}

Output:

-9.421306: x[0.001565 0.001565 0.001565],       x1-x2:0.000000 x1-x3:0.000000 x2-x3:0.000000]
-9.421172: x[0.001431 0.001431 0.001431],       x1-x2:0.000000 x1-x3:0.000000 x2-x3:0.000000]
-9.421039: x[0.001298 0.001298 0.001298],       x1-x2:0.000000 x1-x3:0.000000 x2-x3:0.000000]
-9.420905: x[0.001165 0.001165 0.001165],       x1-x2:0.000000 x1-x3:0.000000 x2-x3:0.000000]
-9.420772: x[0.001032 0.001032 0.001032],       x1-x2:0.000000 x1-x3:0.000000 x2-x3:0.000000]
-6.275573: x[0.001037 0.001037 0.001037],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.275439: x[0.001171 0.001171 0.001171],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.275306: x[0.001304 0.001304 0.001304],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.275172: x[0.001438 0.001438 0.001438],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.275039: x[0.001571 0.001571 0.001571],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.274905: x[0.001705 0.001705 0.001705],       x1-x2:0.000000 x1-x3:174.855813 x2-x3:174.855813]
-6.274772: x[0.001838 0.001838 0.001838],       x1-x2:0.116415 x1-x3:174.855813 x2-x3:174.739398]
Jean-François Fabre

Without more information it's difficult to provide an explanation but I'll try anyway.

The difference between using fmod and "plain subtraction" (or addition) like you're doing is that if the value is way out of range already (like 800000 * M_PI for instance), then the add/subtract method doesn't change the value much (it has little effect) and a very big (in absolute value) angle hits your computation function, without an issue, since no artifact is seen.

Using fmod (or atan2) guarantees that the value is in the range you defined, which isn't the same thing.

Note that doing:

float limit = (8 * 2 * M_PI); 
while(angle > limit) angle -= limit;           
while(angle < 0) angle += limit;               
self->state.angle = a;

would be equivalent (roughly) to fmod (but would be worse than fmod for big values since it introduces floating point accumulation errors because of repeated additions or subtractions).

So if inputting very big values in your computation produces the correct result, then you can wonder if it's wise to normalize your angles instead of leaving that to the math library.

EDIT: the first part of this answer assumed that this super-out-of-bounds case would happen, and further question edits showed that this wasn't the case, so...

The other difference between fmod and 2 tests is that there's no guaranteed that the value is the same if already in range when calling fmod

For instance if the implementation is like value - int(value/modulus)*modulus;, floating point inaccuracy may substract a small value to the original value.

Using atan2f combined with sin ... also changes the result if already in range.

(and even if the value is slightly out of range, adding/subbing like you're doing doesn't involve dividing/truncating/multiplying)

Since you can adjust the value in the range by just adding or subbing once, using fmodf or atan2f is overkill in your case, and you can stick to your simple sub/add (adding an else would save a test: if you just reajusted a too low value, no need to test to see if the value is too big)

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related