0

For the context : I have some old equations made with Mathcad that I would like to rewrite using sympy, so I do have the results I should get.

However, considering the formulas :

uu, vv, rr = sp.symbols("u v r")
h2 = sp.Integral(2*sp.exp( (sp.I*uu*rr**2) /2 )*sp.besselj(0,vv*rr)*rr,(rr,0,1))

If I subs one of the variable (uu or vv) to 0, calculation is correct.

f=sp.lambdify((uu),(sp.Abs(h2.subs(vv,0).doit()))**2)
g=sp.lambdify((vv),(sp.Abs(h2.subs(uu,0).doit()))**2)

However if I want to make a 3D graphic using both variables, results are incorrect.

f2 = sp.lambdify((uu,vv),(sp.Abs(h2.doit()))**2)

f2(5,0) should get 0.57, as for f(5), but it didn't

So I worked around this issue with some loops, instead of lambdifying both variables at the same time :

z_list = []
for j in yy:
    f3=sp.lambdify([uu],sp.re((sp.Abs(h2.subs(vv,j).doit())**2)))
    for i in xx:
        z_list.append(f3(i))

This takes longer to compute, but results are good. Is there something I am missing about integrals or lambdify or combination of the two ?

Moreover, next formula I need to calculate is an integration of previous h2:

I6 = sp.Integral(vv*sp.Abs(h2)**4,(vv,0,10))

Edit from 11-01

Thanks to Oscar Benjamin's answer, the issue in the calculations comes from the returned value of the lambdify that returns only the real part.

I have tested three cases :

f3=sp.lambdify((uu),h2.subs(vv,0).doit())
f4=sp.lambdify((uu),h2.subs(vv,0).evalf())
f5=sp.lambdify((uu),h2.subs(vv,0))

Only f3 returns the value with both real and imaginary part (which explains other calculations issue I had before).

(0.23938885764158258+0.7204574462187735j)
0.23938885764158258
0.23938885764158258

Could the lambdify be called with other modules to prevent that ?

Ruli
  • 2,592
  • 12
  • 30
  • 40
S3bG3rd
  • 1
  • 2
  • Can you be clearer about what is incorrect and how you know that it is incorrect? – Oscar Benjamin Jan 10 '22 at 11:51
  • I tried to lambdify the h2 expression with both variables to calculate for any value of u or v and trace the 3D plot. I have edited the post to be clearer (I hope). – S3bG3rd Jan 10 '22 at 11:56
  • 1
    Doing sp.re(sp.N((sp.Abs(h2.subs(vv,0).subs(uu,5).doit()))**2)) gives the correct answer, so the lambdify step doesn't give the result I would. I tried this but did not manage to make it work for me https://stackoverflow.com/a/51177816/17889644 – S3bG3rd Jan 10 '22 at 12:23

2 Answers2

0

The doit function for integrals is expected to be used to evaluate integrals symbolically:

In [47]: f = Integral(exp(-x**2), (x, 0, oo))

In [48]: f
Out[48]: 
∞        
⌠        
⎮    2   
⎮  -x    
⎮ ℯ    dx
⌡        
0        

In [49]: f.doit()
Out[49]: 
√π
──
2

In [50]: _.evalf()
Out[50]: 0.886226925452758

When that works it's nice because an explicit expression without involving integral signs that can then be evaluated more efficiently numerically. You can do that evaluation with evalf for multiprecision calculations or you can use lambdify and have numpy/scipy etc do the calculation much faster in machine precision floating point.

However not all integrals can be evaluated symbolically so doit will sometimes just return the same integral:

In [53]: h2 = Integral(2*r*exp(I*r**2*u/2)*besselj(0, r*v), (r, 0, 1))

In [54]: h2
Out[54]: 
1                               
⌠                               
⎮         2                     
⎮      ⅈ⋅r ⋅u                   
⎮      ──────                   
⎮        2                      
⎮ 2⋅r⋅ℯ      ⋅besselj(0, r⋅v) dr
⌡                               
0                               

In [55]: h2.doit() # slow
Out[55]: 
  1                             
  ⌠                             
  ⎮       2                     
  ⎮    ⅈ⋅r ⋅u                   
  ⎮    ──────                   
  ⎮      2                      
2⋅⎮ r⋅ℯ      ⋅besselj(0, r⋅v) dr
  ⌡                             
  0  

Here we see that the integral did not evaluate. Sometimes where an integral has symbols other than the integration variable we can get a symbolic result by substituting values in. That's what happens here if you set either u or v to zero:

In [59]: h2.subs({u:0, v:5}).doit()
Out[59]: 
2⋅besselj(1, 5)
───────────────
       5       

In [60]: h2.subs({u:5, v:0}).doit()
Out[60]: 
       5⋅ⅈ      
       ───      
        2       
  2⋅ⅈ⋅ℯ      2⋅ⅈ
- ──────── + ───
     5        5 

Because the integrand became simpler it was possible to evaluate the integrals symbolically. However in this case if neither u or v is zero then the integral cannot be evaluated symbolically:

In [61]: h2.subs({uu:2,vv:3}).doit()
Out[61]: 
  1                           
  ⌠                           
  ⎮       2                   
  ⎮    ⅈ⋅r                    
2⋅⎮ r⋅ℯ    ⋅besselj(0, 3⋅r) dr
  ⌡                           
  0 

I expect that this particular integral does not have a straight-forward analytic formula. I checked Wolfram Alpha which also does not find a symbolic result (only a numeric one):

https://www.wolframalpha.com/input/?i=Integral%282*r*exp%28I*r**2%29*besselj%280%2C+3*r%29%2C+%28r%2C+0%2C+1%29%29

The impossibility of finding analytic results from integrals is a basic fact of symbolics: there do not exist recognisable mathematical functions to express the results of most possible integrals. Even in many cases where the functions do exist there do not exist algorithms that can find them.

So we accept that symbolic integration is impossible which means that there is no point in calling doit which is by far the slowest operation in the code that you are using. The purpose of doit would be to get a symbolic result which we could then use more efficiently for many different values of u and v but instead you are calling doit for every possible value of u and v and every time it does not return something useful.

If symbolic integration is not possible then numeric integration is needed. For numeric integration we first need to substitute values for u and v and then we can use a numeric integration algorithm. Since we don't have a symbolic result for the integral the numeric integration algorithm will need to be run once for each possible value of the parameters. There are two straight-forward ways to do this from SymPy: .evalf or lambdify (note that N(obj) is the same as obj.evalf() and also obj.n()).

Using .evalf is slower because it uses multiprecision arithmetic but that also makes it more accurate. Every digit in the returned result can be reasonably be trusted as correct (unless there'a a bug...) and you can also compute more digits if desired:

In [64]: %time h2.subs({uu:2,vv:3}).evalf()
CPU times: user 1.35 s, sys: 0 ns, total: 1.35 s
Wall time: 1.35 s
Out[64]: 0.236305280388988 + 0.0146270775320418⋅ⅈ

In [65]: %time h2.subs({uu:2,vv:3}).evalf(50)
CPU times: user 3.64 s, sys: 8 ms, total: 3.65 s
Wall time: 3.65 s
Out[65]: 
0.23630528038898808262356997039156552180149071451802 + 0.014627077532041813385103750863443465733130
09416169⋅ⅈ

This is faster than calling doit but is still slow compared top standard machine precision numerical code. It is very accurate though.

For faster evaluation you can use lambdify:

In [66]: f2 = lambdify((uu, vv), h2)

In [67]: %time f2(2, 3)
/home/oscar/current/sympy/sympy.git/38venv/lib/python3.8/site-packages/scipy/integrate/quadpack.py:463: ComplexWarning: Casting complex values to real discards the imaginary part
  return _quadpack._qagse(func,a,b,args,full_output,epsabs,epsrel,limit)
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 438 µs
Out[67]: 0.2363052803889881

This now returns very quickly and gives an accurate result except that it only returns the real part. I guess that the scipy routine only handles real integrals. We can instead do the real and imaginary parts separately e.g. the imaginary part is:

In [91]: %time f2(2, 3)
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 387 µs
Out[91]: 0.01462707753204179
Oscar Benjamin
  • 12,649
  • 1
  • 12
  • 14
  • Thank you for your detailed analysis and answer. I have a better understanding of the `doit` function now. Could you tell how you calculate the imaginary part at the end ? For what I tested this morning, my issue comes from the fact that the lambdify returns only the real part, and I need both real and imaginary part. – S3bG3rd Jan 11 '22 at 07:18
  • I edited the first post with other comments. Thank you for your help. – S3bG3rd Jan 11 '22 at 07:34
  • Looks like I missed out how to do the imaginary part: just lambdify the integral of the imaginary part of the integrand. – Oscar Benjamin Jan 11 '22 at 19:34
0

Using the module "mpmath" when lambdifying the expressions do return the full complex number and not only the real part of it.

f3 = sp.lambdify([uu,vv],h2,modules='mpmath')
f3(5,0)
mpc(real='0.2393888576415826', imag='0.7204574462187735')
S3bG3rd
  • 1
  • 2