使用公式计算反三角函数

3

我一直在尝试创建一个用于计算三角函数的自定义计算器。除了使用Chebyshev多项式和/或Cordic算法外,我还使用了Taylor级数,这些级数准确到小数点后几位。

以下是我创建的简单三角函数计算器,不需要任何模块:

from __future__ import division

def sqrt(n):
  ans = n ** 0.5
  return ans

def factorial(n):
  k = 1
  for i in range(1, n+1):
    k = i * k

  return k 

def sin(d):
  pi = 3.14159265359
  n = 180 / int(d) # 180 degrees = pi radians
  x = pi / n # Converting degrees to radians
  ans = x - ( x ** 3 / factorial(3) ) + ( x ** 5 / factorial(5) ) - ( x ** 7 / factorial(7) ) + ( x ** 9 / factorial(9) )
  return ans 

def cos(d):
  pi = 3.14159265359
  n = 180 / int(d) 
  x = pi / n 
  ans = 1 - ( x ** 2 / factorial(2) ) + ( x ** 4 / factorial(4) ) - ( x ** 6 / factorial(6) ) + ( x ** 8 / factorial(8) )
  return ans 

def tan(d): 
  ans = sin(d) / sqrt(1 - sin(d) ** 2)
  return ans 

很遗憾,我找不到任何可以帮助我解释Python中反三角函数公式的来源。我也尝试了将sin(x)的幂次设为-1(sin(x) ** -1),但结果并非预期。

在Python中,什么是最好的解决方案(最好指使用类似泰勒级数的简单方法,并且具有相似的准确性)?这是否可以使用幂级数或者需要使用CORDIC算法?


1
你对“最佳”的标准是什么?你需要速度吗?准确性?简单性?正确的舍入?需要适用于特定固定精度(例如,对于Python中的float),还是需要适用于任意精度?就目前而言,这个问题太广泛了,无法给出一个好的答案。例如,对于arctan,你可以考虑使用幂级数、参数缩减、通过复对数计算、牛顿方法来计算现有tan的倒数、切比雪夫逼近、这些方法的某种组合等等。另外,避免使用math模块的理由是什么? - Mark Dickinson
2
对于执行反正弦等操作的sin, -1指数纯粹是象征性的。实际上将某物提高到-1的幂会得到它的倒数。 - Ignacio Vazquez-Abrams
2
https://dev59.com/8Ws05IYBdhLWcg3wNPO7 - Dadep
3
从前一项构建每个泰勒级数项更简单(也更快)。例如,对于正弦函数,你可以通过以下方式从第二项得到第三项:x**5/5! = x**3/3! * x**2 / (4*5)。这样,你甚至不需要阶乘函数。 - Mark Dickinson
3
泰勒级数只适用于小的x值。大多数实现都使用一些形式的参数减少来尝试在实际使用某种级数之前将输入值置于一个相当紧密的范围内。值得一看的是一些开源数学库,fdlibm文档和易读性都很好。 - Salix alba
显示剩余10条评论
2个回答

3

这个问题的范围很广,但是以下是一些简单的想法(和代码!)可能会作为计算arctan的起点。首先是老生常谈的泰勒级数。为了简单起见,我们使用固定数量的项;在实践中,您可能希望根据x的大小动态决定要使用的项数,或者引入某种收敛准则。有了固定数量的项,我们可以使用类似于霍纳方案的东西高效地评估。

def arctan_taylor(x, terms=9):
    """
    Compute arctan for small x via Taylor polynomials.

    Uses a fixed number of terms. The default of 9 should give good results for
    abs(x) < 0.1. Results will become poorer as abs(x) increases, becoming
    unusable as abs(x) approaches 1.0 (the radius of convergence of the
    series).
    """
    # Uses Horner's method for evaluation.
    t = 0.0
    for n in range(2*terms-1, 0, -2):
        t = 1.0/n - x*x*t
    return x * t

上面的代码对于小的 x(比如绝对值小于 0.1)可以得到良好的结果,但是随着 x 的增大,精度会下降,并且对于 abs(x) > 1.0,无论我们投入多少项(或多少额外精度),级数都不会收敛。因此,我们需要一种更好的方法来计算较大的 x。一种解决方案是通过参数归约,使用恒等式 arctan(x) = 2 * arctan(x / (1 + sqrt(1 + x^2)))。这给出了以下代码,它建立在 arctan_taylor 的基础上,可以为广泛范围的 x 给出合理的结果(但要注意在计算 x*x 时可能会发生溢出和下溢)。
import math

def arctan_taylor_with_reduction(x, terms=9, threshold=0.1):
    """
    Compute arctan via argument reduction and Taylor series.

    Applies reduction steps until x is below `threshold`,
    then uses Taylor series.
    """
    reductions = 0
    while abs(x) > threshold:
        x = x / (1 + math.sqrt(1 + x*x))
        reductions += 1

    return arctan_taylor(x, terms=terms) * 2**reductions

或者,如果已经存在 tan 的实现,则可以使用传统的根查找方法仅仅找到方程式 tan(y) = x 的解 y。由于 arctan 已经自然地被限制在区间 (-pi/2, pi/2) 中,因此二分查找是有效的:

def arctan_from_tan(x, tolerance=1e-15):
    """
    Compute arctan as the inverse of tan, via bisection search. This assumes
    that you already have a high quality tan function.
    """
    low, high = -0.5 * math.pi, 0.5 * math.pi
    while high - low > tolerance:
        mid = 0.5 * (low + high)
        if math.tan(mid) < x:
            low = mid
        else:
            high = mid
    return 0.5 * (low + high)

最后,只是为了好玩,这里提供一种类似CORDIC的实现方式,它更适合于低级别的实现而不是Python。这里的想法是你预先计算一个arctan值表,包含11/21/4等值,然后使用这些值来计算一般的arctan值,基本上通过计算真实角度的连续逼近来实现。值得注意的是,在预处理步骤之后,arctan计算仅涉及加法、减法和乘以2的幂次方的乘法。(当然,在Python层面上,这些乘法并不比任何其他乘法更有效率,但在接近硬件层面上,这可能会产生很大的差异。)

cordic_table_size = 60
cordic_table = [(2**-i, math.atan(2**-i))
                 for i in range(cordic_table_size)]

def arctan_cordic(y, x=1.0):
    """
    Compute arctan(y/x), assuming x positive, via CORDIC-like method.
    """
    r = 0.0
    for t, a in cordic_table:
        if y < 0:
            r, x, y = r - a, x - t*y, y + t*x
        else:
            r, x, y = r + a, x + t*y, y - t*x
    return r

每种方法都有其优点和缺点,所有代码都可以以无数种方式进行改进。我鼓励你去尝试和探索。
总结一下,在一些不太仔细选择的测试值上调用上述函数的结果如下,与标准库math.atan函数的输出进行比较:
test_values = [2.314, 0.0123, -0.56, 168.9]
for value in test_values:
    print("{:20.15g} {:20.15g} {:20.15g} {:20.15g}".format(
        math.atan(value),
        arctan_taylor_with_reduction(value),
        arctan_from_tan(value),
        arctan_cordic(value),
    ))

我的机器上的输出:

    1.16288340166519     1.16288340166519     1.16288340166519     1.16288340166519
     0.0122993797673      0.0122993797673   0.0122993797673002   0.0122993797672999
  -0.510488321916776   -0.510488321916776   -0.510488321916776   -0.510488321916776
    1.56487573286064     1.56487573286064     1.56487573286064     1.56487573286064

非常感谢您的回答,真的帮助我发现在Python中有很多计算反正切的方法!虽然我在网上找不到它。我认为CORDIC似乎是最准确的方法,但我的意图是正确地理解三角函数和反三角函数的计算,所以在这种情况下准确性和速度并不重要。现在我想我已经了解了大部分的概念。 - ShellRox

1

做任何反函数的最简单方法是使用二分查找。

  1. definitions

    let assume function

    x = g(y)
    

    And we want to code its inverse:

    y = f(x) = f(g(y))
    x = <x0,x1>
    y = <y0,y1>
    
  2. bin search on floats

    You can do it on integer math accessing mantissa bits like in here:

    but if you do not know the exponent of the result prior to computation then you need to use floats for bin search too.

    so the idea behind binary search is to change mantissa of y from y1 to y0 bit by bit from MSB to LSB. Then call direct function g(y) and if the result cross x revert the last bit change.

    In case of using floats you can use variable that will hold approximate value of the mantissa bit targeted instead of integer bit access. That will eliminate unknown exponent problem. So at the beginning set y = y0 and actual bit to MSB value so b=(y1-y0)/2. After each iteration halve it and do as many iterations as you got mantissa bits n... This way you obtain result in n iterations within (y1-y0)/2^n accuracy.

    If your inverse function is not monotonic break it into monotonic intervals and handle each as separate binary search.

    The function increasing/decreasing just determine the crossing condition direction (use of < or >).

C++ acos示例

acos

因此,y = acos(x)x = <-1,+1> , y = <0,M_PI> 上被定义,并且是递减的,因此:

double f64_acos(double x)
    {
    const int n=52;         // mantisa bits
    double y,y0,b;
    int i;
    // handle domain error
    if (x<-1.0) return 0;
    if (x>+1.0) return 0;
    // x = <-1,+1> , y = <0,M_PI> , decreasing
    for (y= 0.0,b=0.5*M_PI,i=0;i<n;i++,b*=0.5)  // y is min, b is half of max and halving each iteration
        {
        y0=y;                   // remember original y
        y+=b;                   // try set "bit"
        if (cos(y)<x) y=y0;     // if result cross x return to original y decreasing is <  and increasing is >
        }
    return y;
    }

我这样测试它:

double x0,x1,y;
for (x0=0.0;x0<M_PI;x0+=M_PI*0.01)  // cycle all angle range <0,M_PI>
    {
    y=cos(x0);              // direct function (from math.h)
    x1=f64_acos(y);         // my inverse function
    if (fabs(x1-x0)>1e-9)   // check result and output to log if error
     Form1->mm_log->Lines->Add(AnsiString().sprintf("acos(%8.3lf) = %8.3lf != %8.3lf",y,x0,x1));
    }

没有发现任何差异……因此实现是正确的。当然,在52位小数上进行二分查找通常比多项式逼近更慢……另一方面,这种实现非常简单……

[注]

如果您不想处理单调区间,可以尝试

由于涉及三角函数,您需要处理奇点以避免NaN或除零等情况……

如果您有兴趣,这里有更多关于二分搜索的例子(主要是整数)


非常感谢您的回答,这个概念讲解得非常详细,二分查找似乎是计算反三角函数最简单的方法。我唯一遇到的小问题是我不太理解C语言。如果能提供Python示例就更好了。 - ShellRox
@ShellRox 我完全不会编写Python,所以无法提供帮助。if条件应该很容易理解,for (bla0; bla1; bla2) { bla3 }将在循环开始前执行bla0,重复循环直到bla1为真,在每次迭代中执行bla3,然后执行bla2并再次测试bla1...常量M_PI=3.1415...,而cos()是弧度余弦函数。 - Spektre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接