2017-10-17 140 views
4

我在写一段代码时遇到了问题,这段代码是在集成过程中围绕一个角度进行的,并且是我正在处理的一个小模拟的一部分。所以基本上这个想法是通过确保它始终具有理智的价值来防止角度变大。我已经尝试了三种不同的方法,我期望得到相同的结果。他们大部分时间都是这样。但是前两个在角度值环绕的地方产生了伪影。当我从角度值生成波形时,由于这些精度错误,我会得到不理想的结果。atan2f vs fmodf vs只是简单的减法

所以第一个方法是这样的(极限角度-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] 
+1

'if(angle> limit)angle - = limit; '不是'while(angle> limit)angle - = limit; '所以基本上如果'angle = 800000 * M_PI'你的最后一个方法不会把你的值放在范围内。一个[mcve]可能很有用,输入值是预期的,而使用'fmod'的“意想不到”(忘记atan2f' ATM) –

+0

屏幕截图看起来并不是在一台机器上完成的,当你使用* double *而不是* float *。 –

+0

该代码只能在32位浮点支持的嵌入式目标上运行。 – Martin

回答

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会节省一个测试:如果你只是调整了一个太低的值,不需要测试,看看是否值太大)

+0

当然可以。我只是想弄清楚为什么不同的结果给出了上面的例子。目的是保持角度不会出现在无穷远处,因此无需一次性将其捕捉到范围内,因为在每次迭代期间它永远不会超过0.5PI。 – Martin

+0

添加了一个小例子。很明显,1e-6值的顺序存在很小的差异。这可能是因为这些小的变化会被放大,并在几次矩阵乘法和积分之后产生实质性的错误。我仍然试图准确地确定使用atan2/fmodf时可能导致问题的原因。 – Martin

+0

好吧,我已经增强了我的答案。仍然无法找出根本原因,但试图解释和证明你的设计选择。 –

3

floatdouble数学。

当然,第三种方法效果最好。它使用double数学。


看看b1, b3b3肯定是由于fmodf()调用而导致精度为float

请注意M_PI通常是double,所以b1 -= 2.f * M_PI;很可能与double精度数学完成,并提供更准确的答案。 f2.f不强制产品2.f * M_PIfloat - 产品是double,因此是-=

b1 -= 2.f * M_PI; 
// same as 
b1 = (float)((double)b1 - (2.f * M_PI)); 

此外:与优化和FLT_EVAL_METHOD > 0,C被允许在比类型的精度更高执行FP代码。 b1可能会在double处计算,即使代码出现float。凭借更高的精度,而事实上,M_PI(有理数)是不完全π(无理数),导致更准确b1fmodf(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;

推荐OP print FLT_EVAL_METHOD以帮助进一步讨论。

#include <float.h> 
printf("%d\n", FLT_EVAL_METHOD); 

OP有2个解决方案:

  1. 使用更广泛的数学像double对于敏感的弧度减少。

    float b3 = fmod(a3, 2 * M_PI); // not fmodf 
    
  2. 不要使用弧度,但角度测量像度或BAM并执行准确的范围减少。在trig调用之前,角度需要度数来进行弧度转换。

    float b3 = fmodf(a3, 360.0f); // use fmodf, a3, b3 are in degrees 
    

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

+0

关于你的最后一点,我曾建议在注释中考虑使用'sinpi()'和'cospi()',这意味着将角度视为π的分数,并且在非常大范围, – njuffa

+0

@njuffa谢谢。 'sinpi(),cospi()'很有用,但不幸的是它们还不是标准C库的一部分。 – chux

+0

这就是为什么我发布合理的C实现,以纪念今年的π日[这里](https://stackoverflow.com/questions/42792939/implementation-of-sinpi-and-cospi-using-standard-c-数学库)。 – njuffa