Python - Неточность при вычислении arcsin - PullRequest
1 голос
/ 11 июля 2020

Я пытаюсь реализовать arcsin в Python без использования какой-либо внешней библиотеки.

Вот мой код:

from time import process_time as pt

class TrigoCalc(metaclass=__readonly):
    # This class evaluates various Trigonometric functions 
    # including Inverse Trigonometric functions
    def __setattr__(self, name, value):
        raise Exception("Value can't be changed")
    

    @staticmethod
    def asin(x):
        '''Implementation from Taylor series
        asin(x) => summation[(2k)! * x^(2k + 1) / (2^(2k) * (k!)^2 * (2k + 1))]
                  k = [0, inf)
        x should be real
        '''
        # a0 = 1                                                                           
        # a1 = 1/(2*3)                                                                     
        # a2 = 1/2 * 3/(4*5) 
        # a3 = 1/2 * 3/4 * 5/(6*7)
        # a4 = 1/2 * 3/4 * 5/6 * 7/(8*9)
        # a5 = 1/2 * 3/4 * 5/6 * 7/8 * 9/(10*11)
        # a6 = 1/2 * 3/4 * 5/6 * 7/8 * 9/10 * 11/(12*13)
        # a7 = 1/2 * 3/4 * 5/6 * 7/8 * 9/10 * 11/12 * 13/(14*15)
        # a8 = 1/2 * 3/4 * 5/6 * 7/8 * 9/10 * 11/12 * 13/14 * 15/(16*17)
        # a9 = 1/2 * 3/4 * 5/6 * 7/8 * 9/10 * 11/12 * 13/14 * 15/16 * 17/(18*19)
        # a10 = 1/2 * 3/4 * 5/6 * 7/8 * 9/10 * 11/12 * 13/14 * 15/16 * 17/18 * 19/(20*21)
        
        # taking 10 coefficients for arriving at a common sequence
        
        # N = n, D = n + 1; (N/D) --> Multiplication, number of times the coefficient number, n >= 1
        
        start_time = pt()

        coeff_list = []
        NUM_ITER = 10000
        for k in range(NUM_ITER):
            if k == 0:
                coeff_list.append(1)
            else:
                N = 1
                D = N + 1
                C = N/D
                if k >= 2:
                    for i in range(k-1):
                        N += 2; D += 2
                        C = C * N/D
                coeff_list.append(C)
        
        __sum = 0
        for k in range(NUM_ITER):
            n = coeff_list[k] * math_utils.power(x, 2*k + 1) / (2*k + 1)
            __sum += n
    
        # Radian conversion to degrees
        __sum = __sum/TrigoCalc.pi * 180
        
        end_time = pt()

        print(f'Execution time: {end_time - start_time} seconds')

        return __sum

Результаты

Когда NUM_ITER составляет 60 (бесконечный ряд, повторяемый 60 раз), имеется значительная неточность в вычислениях на полюсе x = 1, тогда как x = 1/2 дает точность в 14 пунктов.

In [2]: TrigoCalc.asin(0.5)
Execution time: 0.0 seconds
Out[2]: 30.000000000000007

In [3]: TrigoCalc.asin(1)
Execution time: 0.0 seconds
Out[3]: 85.823908877692

Время выполнения в обоих прогонах незаметно .

Когда NUM_ITER равно 10000, то на x = 1 полюсе результат более точен, чем в предыдущем прогоне, но при x = 1/2 точность точно такая же.

In [4]: TrigoCalc.asin(0.5)
Execution time: 19.109375 seconds
Out[4]: 30.000000000000007

In [5]: TrigoCalc.asin(1)
Execution time: 19.109375 seconds
Out[5]: 89.67674183336727 

Время выполнения в этих двух прогонах очень велико для этого типа вычислений.

Проблема

Как я могу сбалансировать код, чтобы он давал как минимум 1-балльную точность при x = 1 полюс в меньшем масштабе NUM_ITER?

Пожалуйста, не стесняйтесь давать предложения или обновления кода.

Python Ver: 3.7.7

РЕДАКТИРОВАТЬ: Изменения в коде для получения точных результатов с помощью ответа @ Joni

  1. Заключение вычисления бесконечной серии в другую функцию внутри asin():

     def asin(x):     
         def __arcsin_calc(x):                                        
             # ....                                                 
             # Computations                                               
             # ....                                                             
             # Removing the radian to degree conversion from this function             
             return __sum            
    
  2. Добавление ограничений к x с помощью новой функции внутри asin() для избежать медленной сходимости:

     if -1.0 <= x < -0.5:
         return -(TrigoCalc.pi/2 - __arcsin_calc(math_utils.power((1 - x*x), 0.5))) / TrigoCalc.pi * 180    # Radian to Degree conversion
    
     elif -0.5 <= x <= 0.5:
         return __arcsin_calc(x)/TrigoCalc.pi * 180
    
     elif 0.5 < x <= 1.0:          
         return (TrigoCalc.pi/2 - __arcsin_calc(math_utils.power((1 - x*x), 0.5))) / TrigoCalc.pi * 180
    
     else:      
         raise ValueError("x should be in range of [-1, 1]")
    
  3. Результаты:

     In [2]: TrigoCalc.asin(0.99)
     Execution time: 0.0 seconds
     Out[2]: 81.89022502527023
    
     In [3]: math.asin(0.99)/TrigoCalc.pi*180
     Out[3]: 81.89038554400582
    
     In [4]: TrigoCalc.asin(1)
     Execution time: 0.0 seconds
     Out[4]: 90.0
    
     In [5]: math.asin(1)/TrigoCalc.pi*180
     Out[5]: 90.0
    

Ответы [ 2 ]

2 голосов
/ 12 июля 2020

Проблема с arcsin(1) заключается в том, что arcsin(x) идет вертикально при x = 1 (производная неограниченно растет). Полиномиальные приближения, такие как ряд Тейлора, не могут угнаться за ним. У вас очень медленная сходимость, и для получения приличного приближения потребуется огромное количество членов. Вам нужно изменить подход к проблеме.

Например, для маленького x, y = sin(pi/2 - x) приблизительно равно 1 - x^2/2, из которого вы можете получить приближение asin(y) = pi/2 - sqrt(2 - 2*y). Это приближение подходит для значений, очень близких к 1 - вы можете использовать его напрямую.

Если вы немного поработаете, вы можете доказать точное идентичность

asin(x) = pi/2 - 2*asin( sqrt( (1-x)/2 ) )

Используя эту идентичность, вы можете вычислить asin(x) для x, близкого к 1, используя существующую функцию asin, которая подходит для x, близкого к 0.

Например: Для вычисления asin(0.99) вы должны вычислить:

asin(0.99) = pi/2 - 2*asin( sqrt( (1-.99)/2 ) )
           = pi/2 - 2*asin( sqrt(.005) )
           = pi/2 - 2*asin(0.07071067811865475)

... и затем вы могли бы использовать свой существующий алгоритм, чтобы получить высококачественное приближение для asin(0.07071067811865475).

Это метод, используемый в реализациях математических библиотек производственного качества - см., Например, OpenLibm или fdlibm .

1 голос
/ 11 июля 2020

Чрезвычайно базовое c приближение даст sum from 0 to N приблизительное arcsin при 1e(-N) (в радианах). Здесь вы даете результат в градусах, поскольку между градусами и радианами существует примерно отношение 1e2, вам нужно будет установить NUM_ITER = 1e(N+2) для приблизительного значения arcsin в 1e(-N).

Таким образом, для ваш конкретный c вопрос, вам нужно проверить с N = 1 (примерно 1 балл), то есть NUM_ITER = 1e(1+2) = 1,000. Это совсем неточно, но дает представление о значении, которое вы ищете.

Тогда, если вы хотите найти точное значение, я не вижу точного математического метода, который можно было бы использовать каждый раз (для независимо от точности x.point). Однако вы можете использовать алгоритм дихотомии, чтобы найти NUM_ITER, если это цель вашего алгоритма. Первое приближение сократит ваше время расчета.

Точное приближение получено из соотношения или x^O(n) и 4^O(n), 4^O(n) больше. Мы можем приблизительно рассчитать сумму с помощью O(1/10^n).

Если кто-то сможет произвести точное вычисление, я буду очень рад это увидеть.

...