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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def assertClose(self, x, y, eps=1e-9):
self.assertCloseAbs(x.real, y.real, eps)
self.assertCloseAbs(x.imag, y.imag, eps)

def assertSameSign(self, x, y):
if copysign(1., x) != copysign(1., y):
self.fail(f'{x!r} and {y!r} have different signs')

def check_div(self, x, y):
"""Compute complex z=x*y, and check that z/x==y and z/y==x."""
z = x * y
Expand Down Expand Up @@ -445,6 +449,63 @@ def test_pow_with_small_integer_exponents(self):
self.assertEqual(str(float_pow), str(int_pow))
self.assertEqual(str(complex_pow), str(int_pow))

# Check that complex numbers with special components
# are correctly handled.
values = [complex(x, y)
for x in [5, -5, +0.0, -0.0, INF, -INF, NAN]
for y in [12, -12, +0.0, -0.0, INF, -INF, NAN]]
for c in values:
with self.subTest(value=c):
self.assertComplexesAreIdentical(c**0, complex(1, +0.0))
self.assertComplexesAreIdentical(c**1, c)
self.assertComplexesAreIdentical(c**2, c*c)
self.assertComplexesAreIdentical(c**3, c*(c*c))
self.assertComplexesAreIdentical(c**3, (c*c)*c)
if not c:
continue
for n in range(1, 9):
with self.subTest(exponent=-n):
self.assertComplexesAreIdentical(c**-n, 1/(c**n))

# Special cases for complex division.
for x in [+2, -2]:
for y in [+0.0, -0.0]:
c = complex(x, y)
with self.subTest(value=c):
self.assertComplexesAreIdentical(c**-1, complex(1/x, -y))
c = complex(y, x)
with self.subTest(value=c):
self.assertComplexesAreIdentical(c**-1, complex(y, -1/x))
for x in [+INF, -INF]:
for y in [+1, -1]:
c = complex(x, y)
with self.subTest(value=c):
self.assertComplexesAreIdentical(c**-1, complex(1/x, -0.0*y))
self.assertComplexesAreIdentical(c**-2, complex(0.0, -y/x))
c = complex(y, x)
with self.subTest(value=c):
self.assertComplexesAreIdentical(c**-1, complex(+0.0*y, -1/x))
self.assertComplexesAreIdentical(c**-2, complex(-0.0, -y/x))

# Test that zeroes has the same sign as small non-zero values.
eps = 1e-8
pairs = [(complex(x, y), complex(x, copysign(0.0, y)))
for x in [+1, -1] for y in [+eps, -eps]]
pairs += [(complex(y, x), complex(copysign(0.0, y), x))
for x in [+1, -1] for y in [+eps, -eps]]
for c1, c2 in pairs:
for n in exponents:
with self.subTest(value=c1, exponent=n):
r1 = c1**n
r2 = c2**n
self.assertClose(r1, r2)
self.assertSameSign(r1.real, r2.real)
self.assertSameSign(r1.imag, r2.imag)
self.assertNotEqual(r1.real, 0.0)
if n != 0:
self.assertNotEqual(r1.imag, 0.0)
self.assertTrue(r2.real == 0.0 or r2.imag == 0.0)

def test_boolcontext(self):
for i in range(100):
self.assertTrue(complex(random() + 1e-6, random() + 1e-6))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix calculation of powers of complex numbers. Small integer powers now produce correct sign of zero components. Negative powers of infinite numbers now evaluate to zero instead of NaN.
Powers of infinite numbers no longer raise OverflowError.
39 changes: 25 additions & 14 deletions Objects/complexobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,23 +333,31 @@ _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);
}
Comment on lines +336 to +340
Copy link
Member

Choose a reason for hiding this comment

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

That looks correct for me, given description of _Py_ADJUST_ERANGE2(). But what we gain here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Without this, complex(inf)**101 raised an OverflowError.

There is no test for this, because the result ((inf+nanj)) is still incorrect -- it should be (inf+0j), as for smaller exponents.

Copy link
Member

Choose a reason for hiding this comment

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

the result ((inf+nanj)) is still incorrect -- it should be (inf+0j), as for smaller exponents.

Not necessary, it's correct in C (no mixed-mode rules for cpow()). BTW, for small exponents we also got (inf+nanj) with this patch (even with n=2).

You are right, (inf+0j) seems to be better. But only iff we will use mixed-mode arithmetic rules for exponentiation, i.e. z**real == cmath.exp(real*cmath.log(z)) (where multiplication taken component-wise):

>>> cmath.log(complex('inf'))
(inf+0j)
>>> cmath.exp(cmath.log(complex('inf'))*2)
(inf+0j)

Unfortunately, it's not easy to fix integer algorithm in such case.

I think the better road would be along #123283. We have two variants.

  1. Drop algorithm for integer exponents. That makes complex math in Python more C-compatible.
  2. Add a version for z**real, that will also handle cases when z has special components if we kept also integer algorithm.

Copy link
Member Author

Choose a reason for hiding this comment

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

In any case it should not be OverflowError.

}
return r;
}

static Py_complex
c_powu(Py_complex x, long n)
{
Py_complex r, p;
long mask = 1;
r = c_1;
p = x;
while (mask > 0 && n >= mask) {
if (n & mask)
r = _Py_c_prod(r,p);
mask <<= 1;
p = _Py_c_prod(p,p);
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;
}
return r;
}
Expand All @@ -358,10 +366,11 @@ static Py_complex
c_powi(Py_complex x, long n)
{
if (n > 0)
return c_powu(x,n);
return c_powu(x, n);
else if (n < 0)
return _Py_rc_quot(1.0, c_powu(x, -n));
else
return _Py_c_quot(c_1, c_powu(x,-n));

return c_1;
}

double
Expand Down Expand Up @@ -737,7 +746,9 @@ complex_pow(PyObject *v, PyObject *w, PyObject *z)
// a faster and more accurate algorithm.
if (b.imag == 0.0 && b.real == floor(b.real) && fabs(b.real) <= 100.0) {
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);
Expand Down
Loading