Каким будет вычислительно более быстрый способ реализовать это двумерное численное интегрирование?

Меня интересует двумерное численное интегрирование. Сейчас я использую scipy.integrate.dblquad, но он очень медленный. Пожалуйста, смотрите код ниже. Мне нужно оценить этот интеграл 100 раз с совершенно разными параметрами. Поэтому я хочу сделать обработку максимально быстрой и эффективной. Код:

import numpy as np
from scipy import integrate
from scipy.special import erf
from scipy.special import j0
import time

q = np.linspace(0.03, 1.0, 1000)

start = time.time()

def f(q, z, t):
    return t * 0.5 * (erf((t - z) / 3) - 1) * j0(q * t) * (1 / (np.sqrt(2 * np.pi) * 2)) * np.exp(
        -0.5 * ((z - 40) / 2) ** 2)


y = np.empty([len(q)])
for n in range(len(q)):
    y[n] = integrate.dblquad(lambda t, z: f(q[n], z, t), 0, 50, lambda z: 10, lambda z: 60)[0]

end = time.time()
print(end - start)

Затраченное время

212.96751403808594

Это уже слишком. Пожалуйста, предложите лучший способ достичь того, что я хочу сделать. Я пытался выполнить поиск, прежде чем прийти сюда, но не нашел решения. Я читал, что quadpy может сделать эту работу лучше и быстрее, но я понятия не имею, как реализовать то же самое. Пожалуйста помоги.


person Shankar_Dutt    schedule 04.04.2020    source источник
comment
Кажется, что ваш код в настоящее время работает, и вы хотите его улучшить. Как правило, эти вопросы слишком самостоятельны для этого сайта, но вам может повезти на CodeReview.SE. Не забудьте прочитать их требования, поскольку они немного более строгие, чем на этом сайте.   -  person David Buck    schedule 04.04.2020
comment
@DavidBuck Большое спасибо за предложение. Если вы так считаете, я бы разместил его на CodeReview. Я разместил его здесь, потому что надеялся получить предложения по улучшению кода. Если другие думают так же, я сниму это. Ваше здоровье :)   -  person Shankar_Dutt    schedule 04.04.2020
comment
@ Дэвид, ты активен на CodeReview и готов ответить там? Если нет, то не рекомендую, особенно для numpy вопросов.   -  person hpaulj    schedule 04.04.2020
comment
Вы должны признать и опираться на помощь, которую вы получили в предыдущем вопросе. И попробуйте применить обратную связь, которую вы получили от предыдущей публикации CR.   -  person hpaulj    schedule 04.04.2020
comment
Для скалярных вычислений math.sqrt и math.expmath.erf) быстрее, чем эквиваленты np.   -  person hpaulj    schedule 04.04.2020
comment
Поскольку границы t и z фиксированы, не могли бы вы сделать то же самое с двумя quad или quad_vec? В предыдущем вопросе quad_vec на самом деле быстрее, чем quadpy.   -  person hpaulj    schedule 04.04.2020
comment
@hpaulj Большое спасибо за ваши ответы. Последний вопрос, который я задал, касался одномерного интеграла. Мне нужна была помощь по интеграции 2D, поэтому я и спросил об этом. Мне очень нравится ваша идея использовать два quad или quad_vec, но я понятия не имею, как использовать в моем случае. Буду признателен за любые предложения/помощь по этому поводу :)   -  person Shankar_Dutt    schedule 04.04.2020
comment
В quadpy пока нет dblquad, но я планировал добавить его на днях.   -  person Nico Schlömer    schedule 05.04.2020
comment
@NicoSchlömer Большое спасибо за ваш ответ. Есть ли более быстрый способ сделать это с помощью quadpy. Как было предложено выше, может быть, использовать quad дважды или любым другим способом?   -  person Shankar_Dutt    schedule 05.04.2020
comment
Отправьте запрос на реализацию dblquad.   -  person Nico Schlömer    schedule 05.04.2020
comment
Ваш пост CR получил хороший ответ, в основном улучшив время выполнения вашего уравнения f(). Я предложил аналогичные вещи с вашим предыдущим вопросом 1d. Мы улучшаем эти quad раза, либо делая f() быстрее, либо используя более умный quad (который может вызывать f меньше раз). quad_vec повышает скорость, запрашивая у f несколько значений одновременно.   -  person hpaulj    schedule 06.04.2020


Ответы (2)


Вы можете использовать Numba или низкоуровневую вызываемую

Почти ваш пример

Я просто передаю функцию непосредственно в scipy.integrate.dblquad вместо вашего метода, использующего лямбда-выражения для генерации функций.

import numpy as np
from scipy import integrate
from scipy.special import erf
from scipy.special import j0
import time

q = np.linspace(0.03, 1.0, 1000)

start = time.time()

def f(t, z, q):
    return t * 0.5 * (erf((t - z) / 3) - 1) * j0(q * t) * (1 / (np.sqrt(2 * np.pi) * 2)) * np.exp(
        -0.5 * ((z - 40) / 2) ** 2)

def lower_inner(z):
    return 10.

def upper_inner(z):
    return 60.


y = np.empty(len(q))
for n in range(len(q)):
    y[n] = integrate.dblquad(f, 0, 50, lower_inner, upper_inner,args=(q[n],))[0]

end = time.time()
print(end - start)
#143.73969149589539

Это уже немного быстрее (143 против 151 с), но единственная польза — простой пример для оптимизации.

Простая компиляция функций с помощью Numba

Чтобы это запустить, вам дополнительно понадобятся Numba и numba-scipy. Целью numba-scipy является предоставление обернутых функций из scipy.special.

import numpy as np
from scipy import integrate
from scipy.special import erf
from scipy.special import j0
import time
import numba as nb

q = np.linspace(0.03, 1.0, 1000)

start = time.time()

#error_model="numpy" -> Don't check for division by zero
@nb.njit(error_model="numpy",fastmath=True)
def f(t, z, q):
    return t * 0.5 * (erf((t - z) / 3) - 1) * j0(q * t) * (1 / (np.sqrt(2 * np.pi) * 2)) * np.exp(
        -0.5 * ((z - 40) / 2) ** 2)

def lower_inner(z):
    return 10.

def upper_inner(z):
    return 60.


y = np.empty(len(q))
for n in range(len(q)):
    y[n] = integrate.dblquad(f, 0, 50, lower_inner, upper_inner,args=(q[n],))[0]

end = time.time()
print(end - start)
#8.636585235595703

Использование вызываемого объекта низкого уровня

Функции scipy.integrate также предоставляют возможность передавать функцию обратного вызова C вместо функции Python. Эти функции можно написать, например, на C, Cython или Numba, которые я использую в этом примере. Основное преимущество заключается в том, что при вызове функции не требуется взаимодействие с интерпретатором Python.

Отличный ответ @Jacques Gaudin показывает простой способ сделать это, включая дополнительные аргументы.

import numpy as np
from scipy import integrate
from scipy.special import erf
from scipy.special import j0
import time
import numba as nb
from numba import cfunc
from numba.types import intc, CPointer, float64
from scipy import LowLevelCallable

q = np.linspace(0.03, 1.0, 1000)

start = time.time()

def jit_integrand_function(integrand_function):
    jitted_function = nb.njit(integrand_function, nopython=True)

    #error_model="numpy" -> Don't check for division by zero
    @cfunc(float64(intc, CPointer(float64)),error_model="numpy",fastmath=True)
    def wrapped(n, xx):
        ar = nb.carray(xx, n)
        return jitted_function(ar[0], ar[1], ar[2])
    return LowLevelCallable(wrapped.ctypes)

@jit_integrand_function
def f(t, z, q):
    return t * 0.5 * (erf((t - z) / 3) - 1) * j0(q * t) * (1 / (np.sqrt(2 * np.pi) * 2)) * np.exp(
        -0.5 * ((z - 40) / 2) ** 2)

def lower_inner(z):
    return 10.

def upper_inner(z):
    return 60.


y = np.empty(len(q))
for n in range(len(q)):
    y[n] = integrate.dblquad(f, 0, 50, lower_inner, upper_inner,args=(q[n],))[0]

end = time.time()
print(end - start)
#3.2645838260650635
person max9111    schedule 07.04.2020

Как правило, выполнять суммирование с помощью матричных операций намного быстрее, чем использовать scipy.integrate.quad (или dblquad). Вы можете переписать свой f (q, z, t), чтобы взять вектор q, z и t и вернуть трехмерный массив f-значений, используя np.tensordot, а затем умножить свой элемент площади (dtdz) на значения функции и сумму их с помощью np.sum. Если ваш элемент области не является постоянным, вы должны создать массив элементов области и использовать np.einsum. Чтобы принять во внимание ваши пределы интеграции, вы можете использовать замаскированный массив для маскировки значений функции за пределами ваших пределов интеграции перед суммированием. Обратите внимание, что np.einsum игнорирует маски, поэтому, если вы используете einsum, вы можете использовать np.where, чтобы установить значения функций за пределами ваших ограничений интеграции равными нулю. Пример (с элементом постоянной площади и простыми пределами интегрирования):

import numpy as np
import scipy.special as ss
import time

def f(q, t, z):

    # Making 3D arrays before computation for readability. You can save some time by
    # Using tensordot directly when computing the output
    Mq = np.tensordot(q, np.ones((len(t), len(z))), axes=0)
    Mt = np.tensordot(np.ones(len(q)), np.tensordot(t, np.ones(len(z)), axes = 0), axes = 0)
    Mz = np.tensordot(np.ones((len(q), len(t))), z, axes = 0)

    return Mt * 0.5 * (ss.erf((Mt - Mz) / 3) - 1) * (Mq * Mt) * (1 / (np.sqrt(2 * np.pi) * 2)) * np.exp(
     -0.5 * ((Mz - 40) / 2) ** 2)

q = np.linspace(0.03, 1, 1000)
t = np.linspace(0, 50, 250)
z = np.linspace(10, 60, 250)

#if you have constand dA you can shave some time by computing dA without using np.diff
#if dA is variable, you have to make an array of dA values and np.einsum instead of np.sum
t0 = time.process_time()
dA = np.diff(t)[0] * np.diff(z)[0]
func_vals = f(q, t, z)
I = np.sum(func_vals * dA, axis=(1, 2))
t1 = time.process_time()

это заняло 18,5 с на моем MacBook Pro 2012 года (2,5 ГГц i5) с dA = 0,04. Подобный подход также позволяет вам легко выбирать между точностью и эффективностью и устанавливать для dA значение, которое имеет смысл, когда вы знаете, как ведет себя ваша функция.

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

person Vegard Jervell    schedule 06.04.2020