-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
BUG: arcsin evaluation on branch cuts deviates from Python array API standard #26310
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
Comments
I doubt it's a numpy issue. Rather it's a known issue of the CPython's complex arithmetic, see this thread. The C11 standard (annex G) has special rules for mixed-mode arithmetic (real+complex and imaginary+complex; most compilers implement at least the first), while the CPython's complex numbers do type casts ( >>> import numpy
>>> z = complex(2, 0)
>>> numpy.arcsin(z)
(1.5707963267948966+1.3169578969248166j)
>>> -1j*numpy.log(z*1j + numpy.sqrt(complex(1 - (z*z).real, -(z*z).imag)))
(1.5707963267948966+1.3169578969248164j) |
It is a numpy issue in the sense that the actual behavior of numpy.arcsin does not match with the (target) promise that NumPy is fully Python array API standard compliant. Although, such issue could be fixed at the documentation level. However, it is not solely of NumPy or CPython issue. The same behavior is present in Boost asin implementation (because the same Hull et al algorithm is used there), for instance. The corrected example is not sufficient to resolve this issue as users are not expected to manipulate their code in the demonstrated way. |
I doubt. The documentation mention mathematical expression, Same issue affects other elementary inverse functions, e.g. >>> numpy.arcsinh(complex(0.0,2))
(1.3169578969248166+1.5707963267948966j)
>>> numpy.arcsinh(complex(-0.0,2))
(-1.3169578969248166+1.5707963267948966j)
>>> z1 = complex(0.0,2)
>>> z=z1; numpy.log(z + numpy.sqrt(1+z*z))
(1.3169578969248166+1.5707963267948966j)
>>> z2 = complex(-0.0,2)
>>> z=z2; numpy.log(z + numpy.sqrt(1+z*z))
(1.3169578969248166+1.5707963267948966j)
Not sure how it's possible. Proper fix would be adding an imaginary type in CPython and implementing all mixed-mode arithmetic rules (like it's specified in the c11's annex G). This will require a PEP. From the quoted thread, I doubt someone from CPython core devs will sponsor this...
Probably, it depends on the complex number backend (mpc, for example). Not all implement special rules for mixed-mode arithmetic. This is valid for the gmpy2: >>> import gmpy2
>>> z = gmpy2.mpc(2, 0)
>>> gmpy2.mpc(0,-1) * gmpy2.log(z*gmpy2.mpc(0,1) + gmpy2.sqrt(1 - z*z))
mpc('1.5707963267948966-1.3169578969248166j') |
OK, I see. So, this issue is a consequence of having defects in the basic complex arithmetic of Python or numpy objects. Say, take >>> z = numpy.array([2, 2], dtype=numpy.complex128)
>>> z.imag[0] *= -1
>>> z
array([2.-0.j, 2.+0.j])
>>> I = numpy.array([0], dtype=numpy.complex128)
>>> I.imag[0] = 1
>>> I
array([0.+1.j]) then the sign bit gets easily lost even in non-mixed-mode arithmetic: >>> z * I
array([0.+2.j, 0.+2.j])
One possibility is to adjust the Python array API standard to use a different definition for Another possibility is to mention in the Python array API standard that on the branch cut, the arcsin expression is not guaranteed to evaluate to the same value with the arcsin return value when expression evaluation is signed-zero ignorant.
Absolutely. The reason is that in the set of functions {arcsinh, arccosh, arcsin, arccos}, one often implements only one of them, say, arcsin, and the rest of the functions can be expressed in terms of arcsin. |
In fact, this is also case of mixed-mode arithmetic:) The C11 has special imaginary types (the macro
Though, neither gcc nor clang support this. |
There doesn't seem to be anything actionable to me here in NumPy, so going to close it. The "array api" even says:
basically with the assumption that C99 is a reasonable expectation. If NumPy fails that, it would most likely (admittedly not necessarily) mean that compilers/math libs used by NumPy ignore C99. I am certain there are some interesting things here, like whether |
(1) These coercion rules break using usual rectangular notation for complex numbers (e.g. in CPython:
(2) Or follow C99, C11, etc. I.e.
On another hand, as it seems from the linked thread, CPython core devs are mostly happy with the present state of art (1) and I think, that any change (i.e. adoption of c99+, (2)) here could come only from numeric libraries, outside of the stdlib. |
Right, the scientific python community could certainly push for such changes and may succeed with sufficient motivation. The main thing is: While I agree in principle with all of these subtleties, the only actual issues I remember seeing are from those explicitly writing tests for them with no motivation beyond testing standards like C99 compliance. (This is not isolated to imaginary numbers, similar things exist with -0.0 handling for float in some places.) |
I agree with @seberg here. Branch cuts are nice to have clearly documented when every array/tensor library is already compliant. The requirements were checked during writing of the spec standard, but there are so many that surely a few mismatches were missed. In such cases, it seems perfectly fine to skip the relevant test and document what the mismatch is - it can be revisited if there's a more compelling reason to put actual work into it. |
If I understand your message correctly, this is a disappointing conclusion for building software quality upon testing (where standards can be used as a reference). In this particular case, the advice seems to be to use
approach which is more annoying compared to agreeing what is the expected result and implementing it accordingly, especially when relevant standards already exists. I realize that this is not an easy problem to resolve, say, within Python array API standard, considering that there exists two groups of major software that define the arcsin value at its branch cut differently for various reasons:
When implementing |
Well, I don't mind if other NumPy devs take up this. But basically, similar libraries seem to agree with NumPy and many of them have far more experience and even if we agree it would make more sense to follow More importantly: I also think there is also way too much half knowledge here and that explicitly includes the NumPy implementation and the Array API. So if you want to make the point, then you really need to dig deeper, and the only thing I can think of is the C99 Annex G (short of some old Kahan paper or so).
Which, translated, means that:
and thus the branch cut must differentiate between EDIT: That Kahan reference, which to me would be authoritative is: "Branch Cuts for Complex Elementary Functions or Much Ado About Nothing's Sign Bit", which I think agrees, but I don't want to read it that in depth... |
That's only part of the fix. Another one is special mixed-mode rules for arithmetic, that include "complex op real" cases and "complex op imaginary" cases. Exactly as specified by Annex G of the C99.
Well, motivation is mostly same as for the C standard. First, we could keep ordinary rectangular notation for complex numbers, without corner cases for special numbers (signed zero, infinities, nans). E.g. to input Second, we could preserve common identities like Sadly, the second argument was missing in the mentioned thread.
Well, many people assume that usual rectangular notation is valid for CPython's complex numbers. (BTW, this is claimed in the CPython docs! See python/cpython#109218) The quoted thread links many CPython's issues, here is another recent example in the mpmath: mpmath/mpmath#774 (comment) (this time related to infinities, not signed zeros).
Floats in CPython mostly are just IEEE 754 floats, I doubt there are much place for confusion. |
Fair, unlike the branch-cuts (which everything I saw until now make me think they are right as they are), there are issues around mixed mode operations. I am not sure I want to follow up on it in NumPy (I think this is something where it would be nice to have Python/others take the lead before taking any real action). |
Describe the issue:
According to Python array API standard,
arcsin(z)
is defined asAs demonstrated below, the evaluation of
numpy.arcsin
and using the definition above lead to different results on the real line whenabs(z.real) > 1
.Reproduce the code example:
Error message:
No response
Python and NumPy Versions:
2.0.0rc1
3.11.8 | packaged by conda-forge | (main, Feb 16 2024, 20:53:32) [GCC 12.3.0]
Runtime Environment:
No response
Context for the issue:
See also mpmath/mpmath#786 that contains some data points on how other software evaluate arcsin on its branch cuts.
The text was updated successfully, but these errors were encountered: