Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

polarify meijerg z param prior to hyperexpand #26551

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

ehren
Copy link
Contributor

@ehren ehren commented Apr 28, 2024

References to other Issues or PRs

partially addresses #26525 (but not the first formula mentioned in the issue title)

Brief description of what is fixed or changed

Other comments

Release Notes

  • simplify
    • polarify meijerg z param prior to hyperexpand to fix some flipped signs / incorrect results from hyperexpand for particular z

@sympy-bot
Copy link

sympy-bot commented Apr 28, 2024

Hi, I am the SymPy bot. I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

  • simplify
    • polarify meijerg z param prior to hyperexpand to fix some flipped signs / incorrect results from hyperexpand for particular z (#26551 by @ehren)

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.13.

Click here to see the pull request description that was parsed.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->

partially addresses #26525 (but not the first formula mentioned in the issue title)

#### Brief description of what is fixed or changed


#### Other comments


#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers. Formerly, `log(-x)` incorrectly gave `-log(x)`.

* physics.units
  * Corrected a semantical error in the conversion between volt and statvolt which
    reported the volt as being larger than the statvolt.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->

* simplify
    * polarify meijerg z param prior to hyperexpand to fix some flipped signs / incorrect results from hyperexpand for particular z

<!-- END RELEASE NOTES -->

@oscarbenjamin
Copy link
Contributor

I have never really understood exactly what polarify and unpolarify are really supposed to be used for or how they should be used correctly. It is clear that there are bugs related to them being used incorrectly but I don't have a clear enough idea of their proper use to judge which part should be considered the bug or what the appropriate fix should be.

Can you explain what the purpose of polarify is especially in relation to meijerg and the integration routines and how the change here improves things (without making other things worse)?

@ehren
Copy link
Contributor Author

ehren commented Apr 28, 2024

This doesn't address this case:

>>> meijerg(((1, 2), ()), ((S(1)/2,), (0,)), 0)
meijerg(((1, 2), ()), ((1/2,), (0,)), 0)
>>> hyperexpand(_)    # prior to this pull this results in -4*sqrt(pi)
0
>>> meijerg(((1, 2), ()), ((S(1)/2,), (0,)), x)
meijerg(((1, 2), ()), ((1/2,), (0,)), x)
>>> hyperexpand(_)
-sqrt(pi)*sqrt(x)*(4*(x/2 + 1/2)/sqrt(x + 1) + 2*asinh(sqrt(x))/sqrt(x))
>>> _.subs(x,0)
nan

but this is the same behavior as mathematica:

https://www.wolframalpha.com/input?i=MeijerG%5B%7B%7B1%2C+2%7D%2C+%7B%7D%7D%2C+%7B%7B1%2F2%7D%2C+%7B0%7D%7D%2C+0%5D

https://www.wolframalpha.com/input?i=MeijerG%5B%7B%7B1%2C+2%7D%2C+%7B%7D%7D%2C+%7B%7B1%2F2%7D%2C+%7B0%7D%7D%2C+x%5D

(If it was worthwhile to address this could be handled similar to the "heurisch wrapper" reintegration which mathematica also doesn't do)

@oscarbenjamin
Copy link
Contributor

This doesn't address this case:

I'm not quite sure what you think should be addressed. The PR gives:

In [1]: e = meijerg(((1, 2), ()), ((S(1)/2,), (0,)), x)

In [2]: hyperexpand(e.subs(x, 0))
Out[2]: 0

In [3]: hyperexpand(e).subs(x, 0)
Out[3]: nan

In [4]: hyperexpand(e).limit(x, 0)
Out[4]: 0

In [5]: e.limit(x, 0)
Out[5]: 0

In [6]: hyperexpand(e)

Out[6]: 
       ⎛  ⎛x   1⎞              ⎞
       ⎜4⋅⎜─ + ─⎟              ⎟
       ⎜  ⎝2   22asinh(√x)⎟
-π⋅√x⋅⎜───────── + ───────────⎟
       ⎜  _______x     ⎟
       ⎝╲╱ x + 1In [7]: hyperexpand(e).expand()
Out[7]: 
        3/2                             
  2⋅√πx       2⋅√π⋅√x                  
- ───────── - ───────── - 2⋅√πasinh(√x)
    _______     _______                 
  ╲╱ x + 1    ╲╱ x + 1                  

In [8]: hyperexpand(e).expand().subs(x, 0)
Out[8]: 0

The hyperexpand(e).subs(x, 0) case does not work because we end up with 0/0. It could be that the sqrt(x) should be cancelled in or that the result from hyperexpand should be a Piecewise.

This is unfortunate (not caused by this PR):

In [9]: e.series(x, n=3)
Out[9]: 
    ╭─╮1, 20, 2    │  ⎞       ╭─╮1, 2-1, 2    │  ⎞   ╭─╮1, 21, 2    │  ⎞    ⎛ 3zoo⋅│╶┐     ⎜        │ 0+ zoo⋅│╶┐     ⎜         │ 0+ │╶┐     ⎜        │ 0+ Ox ⎠
    ╰─╯2, 21/2   0 │  ⎠       ╰─╯2, 21/2   0 │  ⎠   ╰─╯2, 21/2   0 │  ⎠        

In [10]: hyperexpand(e).series(x, n=3)
Out[10]: 
                 3/2       5/2        
           2⋅√πxπx3-4⋅√π⋅√x - ───────── + ─────── + Ox3         10 

@ehren
Copy link
Contributor Author

ehren commented Apr 28, 2024

Can you explain what the purpose of polarify is especially in relation to meijerg and the integration routines and how the change here improves things (without making other things worse)?

I don't have much more understanding beyond what's in the script + comments added in #26551 (never taken even the "complex variables" course). I think I'd have a chance of understanding why this works for the z=0 case (and help(polar_lift) == "Lift argument to the Riemann surface of the logarithm, using the standard branch." but the extraction of -I is even more confusing...

@ehren
Copy link
Contributor Author

ehren commented Apr 28, 2024

The hyperexpand(e).subs(x, 0) case does not work because we end up with 0/0. It could be that the sqrt(x) should be cancelled in or that the result from hyperexpand should be a Piecewise.

I think if we wanted to be fully pedantically correct the result should be a Piecewise

@oscarbenjamin
Copy link
Contributor

I think if we wanted to be fully pedantically correct the result should be a Piecewise

I haven't followed through the code but I wonder if it should be valid to multiple the sqrt(x) in cancelling the one in the denominator. It is possible that the code just does this in a different order:

In [6]: e1, e2, e3 = sqrt(x), x, 1/sqrt(x)

In [7]: e1*(e2 + e3)
Out[7]: 
   ⎛    1 ⎞
√x⋅⎜x + ──⎟
   ⎝    √xIn [8]: e1*e2 + e1*e3
Out[8]: 
 3/2    
x    + 1

Copy link

Benchmark results from GitHub Actions

Lower numbers are good, higher numbers are bad. A ratio less than 1
means a speed up and greater than 1 means a slowdown. Green lines
beginning with + are slowdowns (the PR is slower then master or
master is slower than the previous release). Red lines beginning
with - are speedups.

Significantly changed benchmark results (PR vs master)

Significantly changed benchmark results (master vs previous release)

| Change   | Before [2487dbb5]    | After [b37bfdd3]    |   Ratio | Benchmark (Parameter)                                                |
|----------|----------------------|---------------------|---------|----------------------------------------------------------------------|
| -        | 69.0±1ms             | 43.6±0.2ms          |    0.63 | integrate.TimeIntegrationRisch02.time_doit(10)                       |
| -        | 68.1±0.6ms           | 43.0±0.2ms          |    0.63 | integrate.TimeIntegrationRisch02.time_doit_risch(10)                 |
| +        | 18.3±0.5μs           | 30.9±0.2μs          |    1.69 | integrate.TimeIntegrationRisch03.time_doit(1)                        |
| -        | 5.38±0.03ms          | 2.86±0.01ms         |    0.53 | logic.LogicSuite.time_load_file                                      |
| -        | 74.4±0.3ms           | 28.6±0.2ms          |    0.38 | polys.TimeGCD_GaussInt.time_op(1, 'dense')                           |
| -        | 26.0±0.1ms           | 16.6±0.04ms         |    0.64 | polys.TimeGCD_GaussInt.time_op(1, 'expr')                            |
| -        | 74.9±0.3ms           | 28.7±0.2ms          |    0.38 | polys.TimeGCD_GaussInt.time_op(1, 'sparse')                          |
| -        | 260±2ms              | 124±0.6ms           |    0.48 | polys.TimeGCD_GaussInt.time_op(2, 'dense')                           |
| -        | 264±3ms              | 124±0.7ms           |    0.47 | polys.TimeGCD_GaussInt.time_op(2, 'sparse')                          |
| -        | 658±6ms              | 371±2ms             |    0.56 | polys.TimeGCD_GaussInt.time_op(3, 'dense')                           |
| -        | 659±5ms              | 371±1ms             |    0.56 | polys.TimeGCD_GaussInt.time_op(3, 'sparse')                          |
| -        | 499±2μs              | 286±3μs             |    0.57 | polys.TimeGCD_LinearDenseQuadraticGCD.time_op(1, 'dense')            |
| -        | 1.79±0.01ms          | 1.04±0.01ms         |    0.59 | polys.TimeGCD_LinearDenseQuadraticGCD.time_op(2, 'dense')            |
| -        | 5.85±0.04ms          | 3.11±0.04ms         |    0.53 | polys.TimeGCD_LinearDenseQuadraticGCD.time_op(3, 'dense')            |
| -        | 452±2μs              | 229±2μs             |    0.51 | polys.TimeGCD_QuadraticNonMonicGCD.time_op(1, 'dense')               |
| -        | 1.49±0.01ms          | 685±3μs             |    0.46 | polys.TimeGCD_QuadraticNonMonicGCD.time_op(2, 'dense')               |
| -        | 5.01±0.04ms          | 1.64±0.02ms         |    0.33 | polys.TimeGCD_QuadraticNonMonicGCD.time_op(3, 'dense')               |
| -        | 378±1μs              | 207±1μs             |    0.55 | polys.TimeGCD_SparseGCDHighDegree.time_op(1, 'dense')                |
| -        | 2.51±0ms             | 1.24±0.01ms         |    0.5  | polys.TimeGCD_SparseGCDHighDegree.time_op(3, 'dense')                |
| -        | 9.94±0.1ms           | 4.31±0.02ms         |    0.43 | polys.TimeGCD_SparseGCDHighDegree.time_op(5, 'dense')                |
| -        | 365±2μs              | 169±0.5μs           |    0.46 | polys.TimeGCD_SparseNonMonicQuadratic.time_op(1, 'dense')            |
| -        | 2.53±0.01ms          | 902±3μs             |    0.36 | polys.TimeGCD_SparseNonMonicQuadratic.time_op(3, 'dense')            |
| -        | 9.76±0.03ms          | 2.63±0.02ms         |    0.27 | polys.TimeGCD_SparseNonMonicQuadratic.time_op(5, 'dense')            |
| -        | 1.04±0.01ms          | 421±2μs             |    0.41 | polys.TimePREM_LinearDenseQuadraticGCD.time_op(3, 'dense')           |
| -        | 1.76±0.01ms          | 507±2μs             |    0.29 | polys.TimePREM_LinearDenseQuadraticGCD.time_op(3, 'sparse')          |
| -        | 6.01±0.04ms          | 1.77±0.01ms         |    0.3  | polys.TimePREM_LinearDenseQuadraticGCD.time_op(5, 'dense')           |
| -        | 8.62±0.07ms          | 1.51±0ms            |    0.17 | polys.TimePREM_LinearDenseQuadraticGCD.time_op(5, 'sparse')          |
| -        | 287±1μs              | 64.6±0.09μs         |    0.23 | polys.TimePREM_QuadraticNonMonicGCD.time_op(1, 'sparse')             |
| -        | 3.57±0.04ms          | 395±2μs             |    0.11 | polys.TimePREM_QuadraticNonMonicGCD.time_op(3, 'dense')              |
| -        | 4.06±0.01ms          | 280±1μs             |    0.07 | polys.TimePREM_QuadraticNonMonicGCD.time_op(3, 'sparse')             |
| -        | 7.24±0.08ms          | 1.25±0ms            |    0.17 | polys.TimePREM_QuadraticNonMonicGCD.time_op(5, 'dense')              |
| -        | 8.91±0.03ms          | 834±3μs             |    0.09 | polys.TimePREM_QuadraticNonMonicGCD.time_op(5, 'sparse')             |
| -        | 5.03±0.02ms          | 3.02±0.04ms         |    0.6  | polys.TimeSUBRESULTANTS_LinearDenseQuadraticGCD.time_op(2, 'sparse') |
| -        | 12.3±0.07ms          | 6.51±0.06ms         |    0.53 | polys.TimeSUBRESULTANTS_LinearDenseQuadraticGCD.time_op(3, 'dense')  |
| -        | 22.5±0.1ms           | 9.08±0.01ms         |    0.4  | polys.TimeSUBRESULTANTS_LinearDenseQuadraticGCD.time_op(3, 'sparse') |
| -        | 5.21±0.04ms          | 875±10μs            |    0.17 | polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(1, 'sparse')    |
| -        | 12.5±0.05ms          | 7.05±0.03ms         |    0.57 | polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(2, 'sparse')    |
| -        | 104±0.7ms            | 25.7±0.02ms         |    0.25 | polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(3, 'dense')     |
| -        | 169±1ms              | 54.2±0.2ms          |    0.32 | polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(3, 'sparse')    |
| -        | 173±1μs              | 111±0.5μs           |    0.64 | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(1, 'dense')      |
| -        | 362±0.8μs            | 219±2μs             |    0.6  | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(1, 'sparse')     |
| -        | 4.40±0.01ms          | 843±8μs             |    0.19 | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(3, 'dense')      |
| -        | 5.46±0.07ms          | 385±4μs             |    0.07 | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(3, 'sparse')     |
| -        | 20.4±0.2ms           | 2.81±0.03ms         |    0.14 | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(5, 'dense')      |
| -        | 23.1±0.3ms           | 639±3μs             |    0.03 | polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(5, 'sparse')     |
| -        | 481±0.8μs            | 137±1μs             |    0.29 | polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(1, 'sparse') |
| -        | 4.85±0.01ms          | 619±3μs             |    0.13 | polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(3, 'dense')  |
| -        | 5.41±0.04ms          | 140±1μs             |    0.03 | polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(3, 'sparse') |
| -        | 13.6±0.2ms           | 1.28±0ms            |    0.09 | polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(5, 'dense')  |
| -        | 14.4±0.07ms          | 143±0.8μs           |    0.01 | polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(5, 'sparse') |
| -        | 133±2μs              | 75.2±0.5μs          |    0.56 | solve.TimeMatrixOperations.time_rref(3, 0)                           |
| -        | 250±1μs              | 88.8±0.8μs          |    0.36 | solve.TimeMatrixOperations.time_rref(4, 0)                           |
| -        | 24.4±0.1ms           | 10.4±0.5ms          |    0.43 | solve.TimeSolveLinSys189x49.time_solve_lin_sys                       |
| -        | 28.3±0.2ms           | 15.5±0.1ms          |    0.55 | solve.TimeSparseSystem.time_linsolve_Aaug(20)                        |
| -        | 55.0±0.2ms           | 24.8±0.1ms          |    0.45 | solve.TimeSparseSystem.time_linsolve_Aaug(30)                        |
| -        | 28.3±0.2ms           | 15.2±0.09ms         |    0.54 | solve.TimeSparseSystem.time_linsolve_Ab(20)                          |
| -        | 54.8±0.08ms          | 24.7±0.2ms          |    0.45 | solve.TimeSparseSystem.time_linsolve_Ab(30)                          |

Full benchmark results can be found as artifacts in GitHub Actions
(click on checks at the top of the PR).

@ehren
Copy link
Contributor Author

ehren commented Apr 28, 2024

In [8]: hyperexpand(e).expand().subs(x, 0)
Out[8]: 0

ahh, missed this part. Perhaps a Piecewise is unnecessary. I think we can just disregard my Piecewise comment above

@ehren
Copy link
Contributor Author

ehren commented Apr 29, 2024

The blog of the original meijerint author @ness01 discusses the design of polarify/unpolarify in multiple posts e.g. https://nessgrh.wordpress.com/2011/08/06/branching-once-more/

Regarding

purpose of polarify is especially in relation to meijerg and the integration routines

I would suspect that this change never affects the working of meijerint because for example in the still failing test case (the only one that actually comes from meijerint!):

    meijerg(((1, x + 2), ()), ((x + S(1)/2,), (0,)), z*exp_polar(I*pi))

z has already been "polarified". I suspect meijerint uses G functions with already "polarified" arguments because of the hardcoded use of polar_lift in create_lookup_table although on this last point I'm not too sure.

One quote from the blog: "we let S be the riemann surface of the logarithm, we understand G to be defined thereon " - this doesn't quite explain why mpmath can correctly evaluate G functions with rectangular complex z. Also the Riemann surface of the logarithm is not defined at 0.

Also note that meijerint in the indefinite integral case will never pass a G function with a purely numeric z.

However, the above comments don't address why this change seems to fix the first test case added in the commit:

meijerg(((1, x + 2), ()), ((x + S(1)/2,), (0,)), -I*z)

and also why simply adding z = polarify(z, lift=True) to do_meijer in hyperexpand is not sufficient for this case (it's possible that this commits separate polarifying the factors returned by z.as_ordered_factors is actually papering over an inadequacy of polarify)

One suspicious thing I did find is the mpmath documentation:

https://mpmath.org/doc/current/functions/hypergeometric.html#meijer-g-function

"The default series is chosen based on the degree and in order to be consistent with Mathematica’s. This definition of the Meijer G-function has a discontinuity at |z| = 1 for some orders, which can be avoided by explicitly specifying a series."

Most of the added testcases have |z| = 1

@oscarbenjamin
Copy link
Contributor

Note also the way that mpmath's meijerg function is called by SymPy:

def _eval_evalf(self, prec):
# The default code is insufficient for polar arguments.
# mpmath provides an optional argument "r", which evaluates
# G(z**(1/r)). I am not sure what its intended use is, but we hijack it
# here in the following way: to evaluate at a number z of |argument|
# less than (say) n*pi, we put r=1/n, compute z' = root(z, n)
# (carefully so as not to loose the branch information), and evaluate
# G(z'**(1/r)) = G(z'**n) = G(z).
import mpmath
znum = self.argument._eval_evalf(prec)
if znum.has(exp_polar):
znum, branch = znum.as_coeff_mul(exp_polar)
if len(branch) != 1:
return
branch = branch[0].args[0]/I
else:
branch = S.Zero
n = ceiling(abs(branch/pi)) + 1
znum = znum**(S.One/n)*exp(I*branch / n)
# Convert all args to mpf or mpc
try:
[z, r, ap, bq] = [arg._to_mpmath(prec)
for arg in [znum, 1/n, self.args[0], self.args[1]]]
except ValueError:
return
with mpmath.workprec(prec):
v = mpmath.meijerg(ap, bq, z, r)
return Expr._from_mpmath(v, prec)

@oscarbenjamin
Copy link
Contributor

I've just never put it all together to understand how the branching is supposed to work but you can see that it goes wrong sometimes e.g. taking the integral mentioned in this blog post:
https://nessgrh.wordpress.com/2011/07/22/deciphering-branch-behaviour/

The behaviour today is:

In [25]: i = 1/(x*sqrt(1-x**2))

In [26]: i
Out[26]:
      1
─────────────
     ________2
x⋅╲╱  1 - x

In [27]: i.integrate(x)
Out[27]:
⎧      ⎛11-acosh⎜─⎟  for ──── > 1
⎪      ⎝x⎠      │ 2│
⎪               │x │
⎨
⎪      ⎛1⎞
⎪asin⎜─⎟   otherwise
⎪      ⎝x⎠
⎩

In [28]: i.integrate(x).diff(x)
Out[28]:
⎧             1                    1
⎪───────────────────────────  for ──── > 1________     _______2│
⎪ 211x │
⎪x ⋅  ╱  -1 + ─ ⋅  ╱  1 + ─
⎪   ╲╱        x  ╲╱       x
⎪
⎨           -
⎪     ────────────────         otherwise________21x ⋅   ╱  1 - ──
⎪          ╱        2
⎪        ╲╱        xIn [29]: i.subs(x, 2*I).n()
Out[29]: -0.223606797749979

In [31]: i.integrate(x).diff(x).subs(x, 2*I).n()
Out[31]: 0.223606797749979

@oscarbenjamin
Copy link
Contributor

I don't understand enough about this yet to be confident that this is the right fix. I read about half the blog posts which helped (they are well written) but I am not there yet...

@ehren
Copy link
Contributor Author

ehren commented Apr 30, 2024

No rush, thanks for helping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants