Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wolfram Alpha and scipy.integrate.quad give me different answers for the same integral

Consider the following function:

import numpy as np
from scipy.special import erf

def my_func(x):
    return np.exp(x ** 2) * (1 + erf(x))

When I evaluate the integral of this function from -14 to -4 using scipy's quad function, I get the following result:

In [3]: from scipy import integrate

In [4]: integrate.quad(my_func, -14, -4)
/usr/local/lib/python2.7/dist-packages/scipy/integrate/quadpack.py:289: UserWarning: The maximum number     of subdivisions (50) has been achieved.
  If increasing the limit yields no improvement it is advised to analyze 
  the integrand in order to determine the difficulties.  If the position of a 
  local difficulty can be determined (singularity, discontinuity) one will 
  probably gain from splitting up the interval and calling the integrator 
  on the subranges.  Perhaps a special-purpose integrator should be used.
  warnings.warn(msg)
Out[4]: (0.21896647054443383, 0.00014334175850538866)

That is, about 0.22.

However, when I submit this integral to Wolfram Alpha, I get a very different result:

-5.29326 X 10 ^ 69.

What's the deal? I'm guessing this has to do with the warning scipy has given me. What's the best way to evaluate this integral in python?

NOTE: Increasing the limit changes the warning but leaves the scipy result unchanged:

In [5]: integrate.quad(my_func, -14, -4, limit=10000)
/usr/local/lib/python2.7/dist-packages/scipy/integrate/quadpack.py:289: UserWarning: The occurrence of roundoff error is detected, which prevents 
  the requested tolerance from being achieved.  The error may be 
  underestimated.
  warnings.warn(msg)
Out[5]: (0.21894780966717864, 1.989164129832358e-05)
like image 978
dbliss Avatar asked Feb 09 '23 04:02

dbliss


1 Answers

TL;DR: The integrand is equivalent to erfcx(-x), and the implementation of erfcx at scipy.special.erfcx takes care of the numerical issues:

In [10]: from scipy.integrate import quad

In [11]: from scipy.special import erfcx

In [12]: quad(lambda x: erfcx(-x), -14, -4)
Out[12]: (0.6990732491815446, 1.4463494884581349e-13)

In [13]: quad(lambda x: erfcx(-x), -150, -50)
Out[13]: (0.6197754761443759, 4.165648376274775e-14)

You can avoid the lambda expression by changing the sign of the integration argument and limits:

In [14]: quad(erfcx, 4, 14)
Out[14]: (0.6990732491815446, 1.4463494884581349e-13)

The problem is the numerical evaluation of 1 + erf(x) for negative values of x. As x decreases, erf(x) approaches -1. When you then add 1, you get catastrophic loss of precision, and for sufficiently negative x (specifically x < -5.87), 1 + erf(x) is numerically 0.

Note that the default behavior at Wolfram Alpha suffers from the same problem. I had to click on "More digits" twice to get a reasonable answer.

The fix is to reformulate your function. You can express 1+erf(x) as 2*ndtr(x*sqrt(2)), where ndtr is the normal cumulative distribution function, available from scipy.special.ndtr (see, for example, https://en.wikipedia.org/wiki/Error_function). Here's an alternative version of your function, and the result of integrating it with scipy.integrate.quad:

In [133]: def func2(x):
   .....:     return np.exp(x**2) * 2 * ndtr(x * np.sqrt(2))
   .....: 

In [134]: my_func(-5)
Out[134]: 0.1107029852258767

In [135]: func2(-5)
Out[135]: 0.11070463773306743

In [136]: integrate.quad(func2, -14, -4)
Out[136]: (0.6990732491815298, 1.4469372263470424e-13)

The answer at Wolfram Alpha after clicking on "More digits" twice is 0.6990732491815446...

This is what the plot of the function looks like when you use a numerically stable version:

Plot of the integrand


To avoid overflow or underflow for arguments with very large magnitudes, you can do part of the computation in log-space:

from scipy.special import log_ndtr

def func3(x):
    t = x**2 + np.log(2) + log_ndtr(x * np.sqrt(2))
    y = np.exp(t)
    return y

E.g.

In [20]: quad(func3, -150, -50)
Out[20]: (0.6197754761435517, 4.6850379059597266e-14)

(Looks like @ali_m beat me to it in the new question: Tricking numpy/python into representing very large and very small numbers.)


Finally, as Simon Byrne pointed out in an answer over at Tricking numpy/python into representing very large and very small numbers, the function to be integrated can be expressed as erfcx(-x), where erfcx is the scaled complementary error function. It is available as scipy.special.erfcx.

For example,

In [10]: from scipy.integrate import quad

In [11]: from scipy.special import erfcx

In [12]: quad(lambda x: erfcx(-x), -14, -4)
Out[12]: (0.6990732491815446, 1.4463494884581349e-13)

In [13]: quad(lambda x: erfcx(-x), -150, -50)
Out[13]: (0.6197754761443759, 4.165648376274775e-14)
like image 144
Warren Weckesser Avatar answered Feb 12 '23 00:02

Warren Weckesser