FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

gh-117999: Fix small integer powers of complex numbers by serhiy-storchaka · Pull Request #124243 · python/cpython · GitHub

Repository navigation

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

Merged
serhiy-storchaka merged 11 commits into
python:mainfrom
serhiy-storchaka:complex_powi
Sep 3, 2026
Merged

serhiy-storchaka merged 11 commits into
python:mainfrom
serhiy-storchaka:complex_powi

Conversation

serhiy-storchaka commented Sep 19, 2024 •
edited
Loading

Copy link
Copy Markdown
Member
  • 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).

Copy link
Copy Markdown
Member

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.

skirpichev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Comment thread Lib/test/test_complex.py Outdated
Comment thread Objects/complexobject.c Outdated
Comment thread Objects/complexobject.c
Comment thread Objects/complexobject.c
skirpichev self-requested a review February 4, 2025 01:34

Copy link
Copy Markdown
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.

Copy link
Copy Markdown
Member

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.

skirpichev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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 thread Objects/complexobject.c Outdated
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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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?

Copy link
Copy Markdown
Member

CC @mdickinson
CC @picnixz

skirpichev requested a review from picnixz August 13, 2025 10:23

Copy link
Copy Markdown
Member

FYI, issue for pow(): #137746

picnixz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This looks good to me though I have a small suggestion.

Comment thread Objects/complexobject.c Outdated
else
return _Py_c_quot(c_1, c_powu(x,-n));

return c_1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Took me a while to remember that this was 1 + 0j (maybe rename that variable in a separate issue, e.g., ONE_AS_COMPLEX).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This now used just in one place. If we need this branch - it's better to inline struct value here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If this is the case, then yes, let's inline it.

Comment thread Objects/complexobject.c Outdated
r = _Py_c_prod(r,p);
mask <<= 1;
p = _Py_c_prod(p,p);
assert(n > 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If you want to make this function a bit more natural, I would suggest having if (n == 0) { return c_1; } and remove the assertion n > 0 and just assume n is an unsigned long (or change the assertion to n >= 0). I don't think it's necessary to handle the case n == 0 in c_powi().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

See my patch above: #124243 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Oh I remember this one. I think we can just keep special-casing n == 0 without actually wondering about the cutoff limit. A generic square-and-multiply algorithm is fine IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Well, it's possible to find base and exponent, when binary exponentiation is worse.

E.g. on this branch, if we omit cutoff, on my system I have:

$ ./python -m timeit -u nsec -s 'z=1e-10+1j;e=1e18+0j' 'z**e'
200000 loops, best of 5: 1.39e+03 nsec per loop
$ ./python -m timeit -u nsec -s 'z=1e-10+1j;e=1e18+0j;from _testcapi import _py_c_pow as p' 'p(z, e)'
200000 loops, best of 5: 1.12e+03 nsec per loop

(Though, both results looks to be a garbage.)

skirpichev added a commit to skirpichev/cpython that referenced this pull request Sep 17, 2025
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>

Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

github-actions Bot added the stale Stale PR or inactive for long period of time. label Apr 18, 2026
github-actions Bot removed the stale Stale PR or inactive for long period of time. label Aug 22, 2026

Copy link
Copy Markdown
Member Author

Added cosmetic changes form #118000 (inlining c_1 and INT_EXP_CUTOFF).

serhiy-storchaka merged commit d56f7e2 into python:main Sep 3, 2026
54 checks passed
serhiy-storchaka deleted the complex_powi branch September 3, 2026 08:43
clin1234 pushed a commit to clin1234/cpython that referenced this pull request Sep 12, 2026
…H-124243)

* 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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL