Python:计算锐角的精度失败

时间:2013-06-06 15:36:41

标签: python numpy geometry precision

我们使用以下代码计算两条线之间的锐角。

def AcuteAngle2(line1,line2):
   ''':: line(x1,y1,x2,y2)'''
   u = (line1[2]-line1[0], line1[3]-line1[1])
   v = (line2[2]-line2[0], line2[3]-line2[1])
   return arccos(abs(dot(u,v)/(norm(u)*norm(v))))

它按预期工作。例如:

>>> AcuteAngle2([0,0,1,0],[0,0,0,1])
1.5707963267948966         #in rad = 90 degree

但是我们最近发现它在某些特殊情况下失败了!

>>> AcuteAngle2([0,0,1,0],[0,0,1,0])
0.0

这是正确的,但是:

>>> AcuteAngle2([0,0,1,1],[0,0,1,1])
2.1073424255447017e-08                #failed!

哪个不对!它应该是0.0 任何想法和解决方案?

更新1:
如下所示,在答案中使用Decimal包可能对某些情况有所帮助。然而,我们的问题仍未得到解决,因为(1)有许多代码需要一定的时间来调整每个部分以使用Decimal。此外,(2)性能显着下降。此外,它还需要(3)在处理numpy数组时进行大量更改。因此,它对我们的案例没用。我们正在考虑某种装饰器等,而不会改变事物并保持numpy性能。顺便说一句,有些人可能会建议使用gmpy等多精度软件包,请注意它们需要在代码中进行大量修改,这对我们的情况没有帮助。

3 个答案:

答案 0 :(得分:4)

一种选择是使用decimal模块来提高计算的精确度:

from decimal import Decimal, getcontext

def AcuteAngle2(line1,line2):
   ''':: line(x1,y1,x2,y2)'''
   u = (Decimal(line1[2]-line1[0]), Decimal(line1[3]-line1[1]))
   v = (Decimal(line2[2]-line2[0]), Decimal(line2[3]-line2[1]))
   return arccos(float(abs(dot(u,v)/(norm(u)*norm(v)))))

看起来默认精度为28个位置,您将获得预期答案:

>>> getcontext().prec
28
>>> AcuteAngle2([0,0,1,1],[0,0,1,1])
0.0

答案 1 :(得分:4)

如果您关心准确性,使用arccos作为锐角是一个坏主意。问题是,对于接近0的小角度变化,该角度的余弦几乎不会改变。对于arccos情况是相反的 - 对于余弦角的非常小的变化更多。

在2D和3D中,更好的方法是使用atan2(crossproduct.length,scalarproduct)

在2D中,这变为atan2( dx1*dy2-dx2*dy1 , dx1*dy1+dx2*dy2 )。请注意,您无需规范化向量,因此有两项改进:

  • arccos
  • 没有误差放大
  • 平方根没有其他错误

答案 2 :(得分:2)

在您的示例中,计算dot(u,v)/(norm(u)*norm(v))时会引入舍入误差。对于您的测试值,计算有效2/(sqrt(2)*sqrt(2))。 sqrt(2)的计算值四舍五入为略大于无限精度值的值。

>>> math.sqrt(2)
1.4142135623730951
>>> math.sqrt(2)*math.sqrt(2)
2.0000000000000004
>>> 2/(math.sqrt(2)*math.sqrt(2))
0.9999999999999998
>>> math.acos(2/(math.sqrt(2)*math.sqrt(2)))
2.1073424255447017e-08

@ F.J的decimal模块解决方案将2/(sqrt(2)*sqrt(2))计算得更高。当此值转换为float(通过arccos)时,它将舍入为1.0。

>>> import decimal
>>> decimal.getcontext().sqrt(2)
Decimal('1.414213562373095048801688724')
>>> decimal.getcontext().sqrt(2)**2
Decimal('1.999999999999999999999999999')
>>> 2/decimal.getcontext().sqrt(2)**2
Decimal('1.000000000000000000000000001')
>>> float(2/decimal.getcontext().sqrt(2)**2)
1.0

使用2/(sqrt(2)*sqrt(2))计算decimal,不同的精度突出了另一个问题。

>>> for i in range(10,30):
...   decimal.getcontext().prec=i
...   print i,2/decimal.getcontext().sqrt(2)**2
... 
10 1.000000001
11 0.99999999995
12 1.00000000001
13 1
14 1
15 0.999999999999995
16 1
17 1.0000000000000001
18 1
19 0.9999999999999999995
20 1
21 1
22 0.9999999999999999999995
23 1
24 1
25 0.9999999999999999999999995
26 1.0000000000000000000000001
27 1.00000000000000000000000001
28 1.000000000000000000000000001
29 1

结果可能正好是1,小于1或大于1.如果您可以先取arccos而不先舍入到浮点数,这可能会造成混淆。对于大于1的值,未定义arccos,因此结果为nan。如果看到这种类型的舍入误差在中间值超过1时打破纬度/经度计算。只是增加所有计算的精度,例如从float64到float128,将无法解决问题。它可能会将问题移到另一组值,但仍会出现舍入错误。

更新1

还有其他几种选择。您可以将公式重写为:

def AcuteAngle3(line1,line2):
   ''':: line(x1,y1,x2,y2)'''
   u = (line1[2]-line1[0], line1[3]-line1[1])
   v = (line2[2]-line2[0], line2[3]-line2[1])
   return arccos(sqrt(abs(dot(u,v)**2/(dot(u,u)*dot(v,v)))))

AcuteAngle3可以避免您的原始问题,但dot(u,u)*dot(v,v)可能会舍入到一个数值略小于实际值的值,您可以尝试取值大于的值的arccos 1.(但是对于整个表达式只使用ROUND_UP或ROUND_DOWN将不起作用;我尝试在我的decimal示例中使用不同的舍入模式,并且仍然存在一些舍入“错误”。)

以下函数检查这些异常事件:

def AcuteAngle4(line1,line2):
   ''':: line(x1,y1,x2,y2)'''
   u = (line1[2]-line1[0], line1[3]-line1[1])
   v = (line2[2]-line2[0], line2[3]-line2[1])
   temp = sqrt(abs(dot(u,v)**2/(dot(u,u)*dot(v,v))))
   if temp > 1:
      return 0.0
   else:
      return arccos(temp)

在计算表达式的每个组成部分时,将中间值计算到更高的精度然后向下舍入,或者选择性地向0或远离0舍入,是其他可能性。