Skip to content

gh-117999: Fix small integer powers of complex numbers #124243

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

serhiy-storchaka
Copy link
Member

@serhiy-storchaka serhiy-storchaka commented Sep 19, 2024

  • Fix the sign of zero components in the result. E.g. complex(1,-0.0)**2 now evaluates to complex(1,-0.0) instead of complex(1,0.0).
  • Fix negative small integer powers of infinite complex numbers. E.g. complex(inf)**-1 now evaluates to complex(0,-0.0) instead of complex(nan,nan).
  • Powers of infinite numbers no longer raise OverflowError. E.g. complex(inf)**1 now evaluates to complex(inf) and complex(inf)**0.5 now evaluates to complex(inf,nan).

* Fix the sign of zero components in the result. E.g. complex(1,-0.0)**2
  now evaluates to complex(1,-0.0) instead of complex(1,-0.0).
* Fix negative small integer powers of infinite complex numbers. E.g.
  complex(inf)**-1 now evaluates to complex(0,-0.0) instead of
  complex(nan,nan).
* Powers of infinite numbers no longer raise OverflowError. E.g.
  complex(inf)**1 now evaluates to complex(inf) and complex(inf)**0.5
  now evaluates to complex(inf,nan).
@skirpichev
Copy link
Contributor

I'll take look, but probably already tomorrow :(

Meanwhile, I'm not sure if fix for division should be included here. That does make sense for a separate pr, that will provide mixed-mode arithmetic for complex.

I was thinking (In fact, I did, patch just lacks more tests: skirpichev#4) on splitting out from skirpichev#1 just cases for real op complex, as specified by C99+ (note, that there is no special case for real/complex per standard). That's something unfortunate, but imaginary numbers gone from latest drafts of upcoming C standard.

Copy link
Contributor

@skirpichev skirpichev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks close to #118000. I doubt there is an easy way to fix powi() algorithm to match generic one (same signs, etc).

@skirpichev skirpichev self-requested a review February 4, 2025 01:34
@serhiy-storchaka
Copy link
Member Author

In mathematics, the non-integer power of a complex number is a multi-value function. We can choose the main value, but this is arbitrary. The integer power of a complex number is always defined unambiguously. It is also more useful. So it is expected that the integer power can be calculated with higher precision and produce less exceptions or NaNs that the general real or complex power.

@skirpichev
Copy link
Contributor

We can choose the main value, but this is arbitrary.

No, this will introduce a huge incompatibility, not just with the C standard. z**a (when a single-valued expected) is usually defined as Exp[a Log[z]], where the principal value Log[z] is taken, e.g. as in Mathematica.

So it is expected that the integer power can be calculated with higher precision and produce less exceptions or NaNs

I doubt you can avoid nan component for complex('inf')**2 with the current algorithm.

Copy link
Contributor

@skirpichev skirpichev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns to be same as #118000, modulo changes in _Py_ADJUST_ERANGE2. I stuck with examples like z=1-1j, but probably we can't do anything in such cases.

Now it looks good for me.

Few remarks:

  1. See suggestion.
  2. _Py_ADJUST_ERANGE2 change looks incomplete for me. I think proper fix should include special case for real/integer exponents, just like we did for arithmetic (I'll open an issue.) But if you wish to keep this, lets add tests.

Comment on lines +348 to +360
assert(n > 0);
while ((n & 1) == 0) {
x = _Py_c_prod(x, x);
n >>= 1;
}
Py_complex r = x;
n >>= 1;
while (n) {
x = _Py_c_prod(x, x);
if (n & 1) {
r = _Py_c_prod(r, x);
}
n >>= 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, your tests works fine with my patch from #118000. Here it is, with slightly adjusted formatting (and using new _Py_rc_quot):

diff --git a/Objects/complexobject.c b/Objects/complexobject.c
index b66ebe131a..9dc5d22b37 100644
--- a/Objects/complexobject.c
+++ b/Objects/complexobject.c
@@ -333,19 +333,26 @@ _Py_c_pow(Py_complex a, Py_complex b)
         r.real = len*cos(phase);
         r.imag = len*sin(phase);
 
-        _Py_ADJUST_ERANGE2(r.real, r.imag);
+        if (isfinite(a.real) && isfinite(a.imag)
+            && isfinite(b.real) && isfinite(b.imag))
+        {
+            _Py_ADJUST_ERANGE2(r.real, r.imag);
+        }
     }
     return r;
 }
 
+#define INT_EXP_CUTOFF 100
+
 static Py_complex
 c_powu(Py_complex x, long n)
 {
-    Py_complex r, p;
+    Py_complex p = x, r = n-- ? p : c_1;
     long mask = 1;
-    r = c_1;
-    p = x;
-    while (mask > 0 && n >= mask) {
+    assert(-1 <= n);
+    assert(n < INT_EXP_CUTOFF);
+    while (n >= mask) {
+        assert(mask > 0);
         if (n & mask)
             r = _Py_c_prod(r,p);
         mask <<= 1;
@@ -357,11 +364,10 @@ c_powu(Py_complex x, long n)
 static Py_complex
 c_powi(Py_complex x, long n)
 {
-    if (n > 0)
-        return c_powu(x,n);
+    if (n >= 0)
+        return c_powu(x, n);
     else
-        return _Py_c_quot(c_1, c_powu(x,-n));
-
+        return _Py_rc_quot(1.0, c_powu(x, -n));
 }
 
 double
@@ -751,9 +757,13 @@ complex_pow(PyObject *v, PyObject *w, PyObject *z)
     errno = 0;
     // Check whether the exponent has a small integer value, and if so use
     // a faster and more accurate algorithm.
-    if (b.imag == 0.0 && b.real == floor(b.real) && fabs(b.real) <= 100.0) {
+    if (b.imag == 0.0 && b.real == floor(b.real)
+        && fabs(b.real) <= INT_EXP_CUTOFF)
+    {
         p = c_powi(a, (long)b.real);
-        _Py_ADJUST_ERANGE2(p.real, p.imag);
+        if (isfinite(a.real) && isfinite(a.imag)) {
+            _Py_ADJUST_ERANGE2(p.real, p.imag);
+        }
     }
     else {
         p = _Py_c_pow(a, b);

(asserts are outcome of review, I'm not sure they are useful here.)

What do you think?

@skirpichev
Copy link
Contributor

CC @mdickinson
CC @picnixz

@skirpichev skirpichev requested a review from picnixz August 13, 2025 10:23
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.

2 participants