atan2f vs fmodf vs just plain subtraction

时间:2017-10-17 15:14:43

标签: c math floating-point

我遇到了一段代码问题,我在集成过程中编写了一个包含角度的代码,这是我正在进行的小型模拟的一部分。因此,基本上这个想法是通过确保角度始终具有合理的值来防止角度变大。我尝试了三种不同的方法,我希望能给出相同的结果。而且大部分时间都是这样。但是前两个在角度值环绕的点附近产生了伪影。然后当我从角度值生成波形时,由于这些精度误差,我得到了不希望的结果。

所以第一种方法是这样的(将角度限制在-8PI + 8PI范围内):

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

这会创建如下所示的工件: enter image description here

第二种方法:

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

创建相同的结果: enter image description here

但是,如果我这样做:

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

然后它按预期工作,没有任何工件: enter image description here

那我在这里错过了什么?为什么其他两种方法会产生精度误差?我希望它们都能产生相同的结果(我知道角度的范围是不同的,但是当角度进一步传递到sin函数时,我希望结果是相同的)。

编辑:小测试

// 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;
}

输出:

-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]

2 个答案:

答案 0 :(得分:3)

如果没有更多信息,很难提供解释,但无论如何我都会尝试。

使用fmod和“普通减法”(或添加)之间的区别就在于,如果值已超出范围(例如800000 * M_PI),那么加/减方法不会更改值(它的影响很小),并且非常大(绝对值)的角度会影响您的计算函数,而不会出现问题,因为没有看到伪影。

使用fmod(或atan2)可以保证该值在您定义的范围内,这不是一回事。

请注意:

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

等价(大致)到fmod(但是对于大值会比fmod更差,因为它会因重复的加法或减法而引入浮点累积误差。

因此,如果在计算中输入非常大的值会产生正确的结果,那么您可能想知道将角度标准化而不是将其留在数学库中是否明智。

编辑:这个答案的第一部分假设这种超出界限的情况会发生,进一步的问题编辑表明情况并非如此,所以......

fmod和2次测试之间的另一个区别是,如果在调用fmod时已经在范围内,则无法保证该值相同

例如,如果实现类似于value - int(value/modulus)*modulus;,浮点不准确可能会将一个小值减去原始值。

使用atan2f结合sin ...也会更改结果(如果已经在范围内)。

(即使该值稍微超出范围,添加/ subbing就像你正在做的那样不涉及分割/截断/乘法)

由于您只需添加或减少一次就可以调整范围内的值,因此在您的情况下使用fmodfatan2f是过度的,您可以坚持使用简单的子/添加(添加else会保存一个测试:如果你只是重新调整了一个太低的值,就不需要测试以查看该值是否太大了)

答案 1 :(得分:3)

floatdouble数学。

当然第3种方法效果最好。它正在使用double数学。

看看b1, b3。由于b3调用,float确实以fmodf()精度计算M_PI

请注意,double通常为b1 -= 2.f * M_PI;,因此double可能会使用f精度数学运算完成,并提供更准确的答案。 2.f中的2.f * M_PI不会强制将产品float加入double - 产品为-=,因此b1 -= 2.f * M_PI; // same as b1 = (float)((double)b1 - (2.f * M_PI));

FLT_EVAL_METHOD > 0

此外:通过优化和b1,允许C以高于类型精度的方式执行FP代码。 double可以float计算,即使代码显示为M_PI。具有更高的精度,以及b1(有理数)不完全是π(无理数)的事实,导致fmodf(a3, 2 * M_PI);float b1 = a1; if(b1 > 2.f * M_PI) b1 -= 2.f * M_PI; // double math if(b1 < 0.f) b1 += 2.f * M_PI; // double math float b3 = fmodf(a3, 2 * M_PI); 更准确

float

要确保volatile float b1 = a1;结果,请使用float进行公平比较,并使用#define M_PIf ((float) M_PI)if(b1 < -2.f * M_PIf) b1 += 2.f * M_PIf;常量

此外。通过公平比较,更好地使用FLT_EVAL_METHOD

推荐OP print #include <float.h> printf("%d\n", FLT_EVAL_METHOD); 以帮助进一步讨论。

double
OP有2个解决方案:

  1. 使用更宽的数学运算,如float b3 = fmod(a3, 2 * M_PI); // not fmodf 来进行敏感的弧度减少。

    float b3 = fmodf(a3, 360.0f);  // use fmodf, a3, b3 are in degrees
    
  2. 不要使用弧度,而是使用度数或BAM进行角度测量,并执行精确的范围缩小。在三角调用之前,角度需要度数来进行弧度转换。

    float b2 = atan2f(sinf(a2), cosf(a2));
  3. 注意:{{1}}方法不是一个合理的竞争者。