Есть ли более быстрый способ объединения функций, содержащих mpf (mpmath floats) с массивами numpy?

У меня были проблемы с точностью поплавков в Python. Мне нужна высокая точность, потому что я хочу использовать явно написанные сферические функции Бесселя J_n (x), которые отклоняются (особенно для n>5) от их теоретических значений при низких значениях x, если numpy используются поплавки (15 точных цифр).

Я перепробовал много вариантов, особенно из mpmath а также sympy, чтобы сохранить более точные цифры. У меня были проблемы при совмещении точности mpmath внутри функций с numpy массивы, пока я не знал, что была функция numpy.vectorize, Наконец я получил решение моей первоначальной проблемы:

import time
% matplotlib qt
import scipy
import numpy as np
from scipy import special
import matplotlib.pyplot as plt
from sympy import *
from mpmath import *
mp.dps=100


#explicit inaccurate
def bessel6_expi(z):
    return -((z**6-210*z**4+4725*z**2-10395)*np.sin(z)+(21*z**5-1260*z**3+10395*z)*np.cos(z))/z**7

#explicit inaccurate 1, computation time increases, a bit less inaccuracy
def bessel6_exp1(z):
    def bv(z):
        return -((z**6-210*z**4+4725*z**2-10395)*mp.sin(z)+(21*z**5-1260*z**3+10395*z)*mp.cos(z))/z**7
    bvec=np.vectorize(bv)
    return bvec(z)

#explicit accurate 2, computation time increases markedly, accurate
def bessel6_exp2(z):
    def bv(z):
        return -((mpf(z)**mpf(6)-mpf(210)*mpf(z)**mpf(4)+mpf(4725)*mpf(z)**mpf(2)-mpf(10395))*mp.sin(mpf(z))+(mpf(21)*mpf(z)**mpf(5)-mpf(1260)*mpf(z)**mpf(3)+mpf(10395)*mpf(z))*mp.cos(mpf(z)))/mpf(z)**mpf(7)
    bvec=np.vectorize(bv)
    return bvec(z)

#explicit accurate 3, computation time increases markedly, accurate
def bessel6_exp3(z):
    def bv(z):
        return -((mpf(z)**6-210*mpf(z)**4+4725*mpf(z)**2-10395)*mp.sin(mpf(z))+(21*mpf(z)**5-1260*mpf(z)**3+10395*mpf(z))*mp.cos(mpf(z)))/mpf(z)**7
    bvec=np.vectorize(bv)
    return bvec(z)

#implemented in scipy, accurate, fast
def bessel6_imp(z):
    def bv(z):
        return scipy.special.sph_jn(6,(z))[0][6] 
    bvec=np.vectorize(bv)
    return bvec(z)

a=np.arange(0.0001,17,0.0001)

plt.figure()
start = time.time()
plt.plot(a,bessel6_expi(a),'b',lw=1,label='expi')
end = time.time()
print(end - start)

start = time.time()
plt.plot(a,bessel6_exp1(a),'m',lw=1,label='exp1')
end = time.time()
print(end - start)

start = time.time()
plt.plot(a,bessel6_exp2(a),'c',lw=3,label='exp2')
end = time.time()
print(end - start)

start = time.time()
plt.plot(a,bessel6_exp2(a),'y',lw=5,linestyle='--',label='exp3')
end = time.time()
print(end - start)

start = time.time()
plt.plot(a,bessel6_imp(a),'r',lw=1,label='imp')
end = time.time()
print(end - start)

plt.ylim(-0.5/10**7,2.5/10**7)
plt.xlim(0,2.0)
plt.legend()

plt.show()

Проблема, с которой я столкнулся сейчас, заключается в том, что для построения точных и точных графиков требуется достаточно много времени (примерно в 31 раз медленнее, чем функция scipy для mp.dps=100). Меньшие dps не ускоряйте эти процессы даже при mp.dps=15они все еще в 26 раз медленнее. Есть ли способ сделать это быстрее?

1 ответ

Решение

Обратите внимание, что потеря точности, которую вы наблюдаете вблизи нуля, происходит из-за того, что вы вычитаете два почти равных члена как формы 10395 z^-6 + O(z^-4), Как истинное значение 1/135135 z^6 + O(z^8) вы потеряете фактор ~1.4 x 10^9 z^-12 в точности. Так что если вы хотите рассчитать значение в z=0.01 скажем, до 7 десятичных знаков, вам нужно начать с точностью>40 десятичных знаков.

Решение, конечно, избежать этой отмены. Прямой способ достижения этого состоит в том, чтобы вычислить степенной ряд около 0.

Вы могли бы использовать sympy чтобы получить степенной ряд:

>>> z = sympy.Symbol('z')
>>> f = -((z**6-210*z**4+4725*z**2-10395)*sympy.sin(z)+(21*z**5-1260*z**3+10395*z)*sympy.cos(z))/z**7
>>> f.nseries(n=20)
z**6/135135 - z**8/4054050 + z**10/275675400 - z**12/31426995600 + z**14/5279735260800 - z**16/1214339109984000 + z**18/364301732995200000 + O(z**20)

Для маленьких z небольшое количество терминов, кажется, достаточно для хорошей точности.

>>> ply = f.nseries(n=20).removeO().as_poly()
>>> float(ply.subs(z, 0.1))
7.397541093587708e-12

Вы можете экспортировать коэффициенты для использования с NumPy.

>>> monoms = np.array(ply.monoms(), dtype=int).ravel()
>>> coeffs = np.array(ply.coeffs(), dtype=float)
>>> 
>>> (np.linspace(-0.1, 0.1, 21)[:, None]**monoms * coeffs).sum(axis=1)
array([7.39754109e-12, 3.93160564e-12, 1.93945374e-12, 8.70461282e-13,
       3.45213317e-13, 1.15615481e-13, 3.03088138e-14, 5.39444356e-15,
       4.73594159e-16, 7.39998273e-18, 0.00000000e+00, 7.39998273e-18,
       4.73594159e-16, 5.39444356e-15, 3.03088138e-14, 1.15615481e-13,
       3.45213317e-13, 8.70461282e-13, 1.93945374e-12, 3.93160564e-12,
       7.39754109e-12])
Другие вопросы по тегам