ENH: special: Extend Riemann Zeta function to complex inputs#21744
Conversation
mdhaber
left a comment
There was a problem hiding this comment.
Great! High level comments to start; I'll take a look at the details this afternoon.
Co-authored-by: Matt Haberland <[email protected]>
There was a problem hiding this comment.
Math looks pretty good except that the
Co-authored-by: Matt Haberland <[email protected]>
| # reference = complex(mp.zeta(t)) | ||
| # cases.append((complex(t), reference, default_rtol)) | ||
|
|
||
| # # Very large imaginary part |
There was a problem hiding this comment.
How long do these take for you to run? (I know it doesn't really matter since it's all offline, but it is taking forever.)
There was a problem hiding this comment.
Extremely fast
In [2]: %timeit run zeta_cases.py
23.1 ms ± 136 μs per loop (mean ± std. dev. of 7 runs, 1 loop each)
Note sure what's going on. My mpmath version is 1.3.0 and I just use the default dps.
|
For the failing test cases, I think it's fine to punt on the Intel oneAPI failures, and I've attempted to |
Co-authored-by: Matt Haberland <[email protected]>
There was a problem hiding this comment.
OK, I wrote before:
Math looks pretty good except that the
$\log \sin$ part, asymptotic approximation of$\frac{|B_{2n}|}{(2k)!}$ ), the last term of the EM formula, and the EM estimate are much harder to follow than the rest.
I've checked the
I tried checking the -z * log(n) in log_factor instead of log(b) and changing the name of the loop variable. Whatever happens, next time I sit down I'm sure I'll be able to verify it. I also need to verify some of the new mpmath test cases at my home computer, since they were running too slowly on Colab.
Then I'll probably play with this locally a bit and do one more superficial pass, and I'll probably be happy with it.
Do you also want someone more familiar with the special/C++-specific stuff to review from that perspective, or are you comfortable with that as long as I've checked the algorithmic stuff?
Noted. Things should be more clear now.
Feel free to merge based on your algorithmic review. This is all pretty straightforward from a C++ perspective. |
Co-authored-by: Matt Haberland <[email protected]>
|
Playing around with this, I think it's pretty good. I think you already knew that 0 + 0j is a problem spot. (The positive axis labels are something like z = (1e-15 + 1e-15j)
res = complex(scipy_zeta(z))
ref = complex(mpmath_zeta(z))
print(res, ref, abs(res-ref)/abs(ref))
# (-0.5097353193036964+0.010130110463593661j) (-0.5000000000000009-9.189385332046748e-16j) 0.02809950746540121Similar story at 1 + 0j. z = (1+1e-15 + 1e-15j)
res = complex(scipy_zeta(z))
ref = complex(mpmath_zeta(z))
print(res, ref, abs(res-ref)/abs(ref))
# (496745969635765.9-443048778321942.06j) (497279149540589.06-447909238514022.3j) 0.007306002718971802At negative even integers (trivial zeros), there is only a problem on the real axis (at least imaginary offsets greater than z = (-2+1e-15)
res = complex(scipy_zeta(z))
ref = complex(mpmath_zeta(z))
print(res, ref, abs(res-ref)/abs(ref))
# (-3.6806848447762256e-17+0j) (-3.3804578090538615e-17+0j) 0.08881253743746413But the absolute error is OK, and this is a problem with the existing real implementation, not this PR. It might be worth a second look at whether we should just use the complex implementation even when the input is real. IIRC, there were problems at the trivial zeros in my implementation, so I replaced it on the entire real axis, but maybe we should just special-case the trivial zeros. When we zoom out, though, the error is typically quite good. Consider white to mean esentially zero error: on the left, both Looking near the critical strip, we get some inaccuracy near the transition between algorithms. But typically the error near the critical strip is not bad. It seems fine very close to the real axis. There is a transition near 50 + 50j that looks fine. The transitions at 50 - 50j looks about the same, and the transitions at the negative reals are invisible, probably because the error is all due to use of the log-reflection (but it's on the order of In the critical strip with imaginary part close to z = 2.51 + 1e13*1j
res = complex(scipy_zeta(z))
ref = complex(mpmath_zeta(z))
print(res, ref, abs(res-ref)/abs(ref))
# (0.9381747827300801-0.21060417060191572j) (0.938250825152661-0.2106505908517201j) 9.26485094426602e-05But I think this PR is about the main algorithms, not the fine tuning or special cases. We know where some additional work may be needed, but let's merge the main implementation. Thanks @steppi! (I'll merge later tonight if you're happy, too.) |
|
Was just updating the release notes for the factorial changes, and noticed that this doesn't have an entry yet. |
Thanks @h-vetinari. There's a few other things I need to add release notes for too. I'll try to get them all done by the end of the month. |








Reference issue
Closes #14073
What does this implement/fix?
This PR extends the Riemann Zeta function to complex inputs. It follows a Python reference implementation written by @mdhaber and posted in a Colab notebook here. There are a couple of minor tweaks I'm aware of which could improve accuracy slightly, but I will leave those for a follow-up to make this easier to review. As can be seen in the linked notebook, results are sufficiently accurate for this to get in as is.
The primary ways the implementation here differs from the reference implementation are that
xsf. I added simple (private) implementation of complexexppi($exp(\pi z)$) for use here.I've added test cases comparing with the
mpmathreference implementation.Additional information
Some things that may need attention:
The Riemann zeta function and the Hurwitz Zeta function are currently combined into a single function
zetawhich dispatches to the ufunc_riemann_zetawhen an argumentq=None, and otherwise dispatches to a ufunc_zetafor the Hurwitz zeta function. I've only added complex support for_riemann_zeta, andzetawill error out when called on complex input whenqis notNone. Having accepted input types depend on a keyword argument isn't ideal, but I think it's acceptable here, and complex support for Hurwitz Zeta is on the way (@mdhaber has written a reference implementation for this too). We could However change this so that complex inputs are still accepted whenqis notNone, butNaNis returned when off the real line. This is similar to what currently happens for inputsx < 1. They are only supported forq = None, but not for any otherq.Is there a better place for the Python reference implementation than a colab notebook? Perhaps we could add reference implementations to the
xsfrepo? I think it's fine not to worry about this now though.