From f2280bec1b1863a5303b981537ec47f8d269bfb0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 19 Sep 2024 15:47:55 +0300 Subject: [PATCH 1/4] gh-117999: Fix small integer powers of complex numbers * 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). --- Lib/test/test_complex.py | 29 +++++ ...-09-19-15-47-50.gh-issue-117999.Iq4jEG.rst | 2 + Objects/complexobject.c | 101 +++++++++++++++--- 3 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-09-19-15-47-50.gh-issue-117999.Iq4jEG.rst diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index ecc97315e50d31..7b7639f2c26dd1 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -366,6 +366,35 @@ 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. + for x in [2, -2, +0.0, -0.0, INF, -INF, NAN]: + for y in [2, -2, +0.0, -0.0, INF, -INF, NAN]: + c = complex(x, y) + with self.subTest(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**4, (c*c)*(c*c)) + self.assertComplexesAreIdentical(c**5, c*((c*c)*(c*c))) + for x in [+2, -2]: + for y in [+0.0, -0.0]: + with self.subTest(complex(x, y)): + self.assertComplexesAreIdentical(complex(x, y)**-1, complex(1/x, -y)) + with self.subTest(complex(y, x)): + self.assertComplexesAreIdentical(complex(y, x)**-1, complex(y, -1/x)) + for x in [+INF, -INF]: + for y in [+1, -1]: + c = complex(x, y) + with self.subTest(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(c): + self.assertComplexesAreIdentical(c**-1, complex(+0.0*y, -1/x)) + self.assertComplexesAreIdentical(c**-2, complex(-0.0, -y/x)) + def test_boolcontext(self): for i in range(100): self.assertTrue(complex(random() + 1e-6, random() + 1e-6)) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-09-19-15-47-50.gh-issue-117999.Iq4jEG.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-19-15-47-50.gh-issue-117999.Iq4jEG.rst new file mode 100644 index 00000000000000..7c3a9b38372cad --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-19-15-47-50.gh-issue-117999.Iq4jEG.rst @@ -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. diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 787235c63a6be1..e64ec8d9ff3863 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -21,10 +21,10 @@ class complex "PyComplexObject *" "&PyComplex_Type" #include "clinic/complexobject.c.h" -/* elementary operations on complex numbers */ - static Py_complex c_1 = {1., 0.}; +/* elementary operations on complex numbers */ + Py_complex _Py_c_sum(Py_complex a, Py_complex b) { @@ -143,6 +143,64 @@ _Py_c_quot(Py_complex a, Py_complex b) return r; } +Py_complex +_Py_dc_quot(double a, Py_complex b) +{ + /* Divide a real number by a complex number. + * Same as _Py_c_quot(), but without imaginary part. + */ + Py_complex r; /* the result */ + const double abs_breal = b.real < 0 ? -b.real : b.real; + const double abs_bimag = b.imag < 0 ? -b.imag : b.imag; + + if (abs_breal >= abs_bimag) { + /* divide tops and bottom by b.real */ + if (abs_breal == 0.0) { + errno = EDOM; + r.real = r.imag = 0.0; + } + else { + const double ratio = b.imag / b.real; + const double denom = b.real + b.imag * ratio; + r.real = a / denom; + r.imag = (- a * ratio) / denom; + } + } + else if (abs_bimag >= abs_breal) { + /* divide tops and bottom by b.imag */ + const double ratio = b.real / b.imag; + const double denom = b.real * ratio + b.imag; + assert(b.imag != 0.0); + r.real = (a * ratio) / denom; + r.imag = (-a) / denom; + } + else { + /* At least one of b.real or b.imag is a NaN */ + r.real = r.imag = Py_NAN; + } + + /* Recover infinities and zeros that computed as nan+nanj. See e.g. + the C11, Annex G.5.2, routine _Cdivd(). */ + if (isnan(r.real) && isnan(r.imag)) { + if (isinf(a) + && isfinite(b.real) && isfinite(b.imag)) + { + const double x = copysign(isinf(a) ? 1.0 : 0.0, a); + r.real = Py_INFINITY * (x*b.real); + r.imag = Py_INFINITY * (- x*b.imag); + } + else if ((isinf(abs_breal) || isinf(abs_bimag)) + && isfinite(a)) + { + const double x = copysign(isinf(b.real) ? 1.0 : 0.0, b.real); + const double y = copysign(isinf(b.imag) ? 1.0 : 0.0, b.imag); + r.real = 0.0 * (a*x); + r.imag = 0.0 * (-a*y); + } + } + + return r; +} #ifdef _M_ARM64 #pragma optimize("", on) #endif @@ -174,7 +232,11 @@ _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; } @@ -182,15 +244,19 @@ _Py_c_pow(Py_complex a, Py_complex b) 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; } @@ -199,10 +265,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_dc_quot(1.0, c_powu(x, -n)); else - return _Py_c_quot(c_1, c_powu(x,-n)); - + return c_1; } double @@ -569,7 +636,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); From 13044239f6bc17b06dd59f3666ed81599bcd91bd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 26 Nov 2024 21:23:51 +0200 Subject: [PATCH 2/4] Polish tests. --- Lib/test/test_complex.py | 43 +++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 8a7c9af7e72d74..e1fe702bc5b8ea 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -430,30 +430,41 @@ def test_pow_with_small_integer_exponents(self): # Check that complex numbers with special components # are correctly handled. - for x in [2, -2, +0.0, -0.0, INF, -INF, NAN]: - for y in [2, -2, +0.0, -0.0, INF, -INF, NAN]: - c = complex(x, y) - with self.subTest(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**4, (c*c)*(c*c)) - self.assertComplexesAreIdentical(c**5, c*((c*c)*(c*c))) + 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**4, (c*c)*(c*c)) + self.assertComplexesAreIdentical(c**5, c*((c*c)*(c*c))) + self.assertComplexesAreIdentical(c**6, (c*c)*((c*c)*(c*c))) + self.assertComplexesAreIdentical(c**7, c*(c*c)*((c*c)*(c*c))) + self.assertComplexesAreIdentical(c**8, ((c*c)*(c*c))*((c*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)) for x in [+2, -2]: for y in [+0.0, -0.0]: - with self.subTest(complex(x, y)): - self.assertComplexesAreIdentical(complex(x, y)**-1, complex(1/x, -y)) - with self.subTest(complex(y, x)): - self.assertComplexesAreIdentical(complex(y, x)**-1, complex(y, -1/x)) + 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(c): + 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(c): + with self.subTest(value=c): self.assertComplexesAreIdentical(c**-1, complex(+0.0*y, -1/x)) self.assertComplexesAreIdentical(c**-2, complex(-0.0, -y/x)) From bc4adfd45f0861ca07897375ff4d37f8d6f57029 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 26 Nov 2024 21:24:57 +0200 Subject: [PATCH 3/4] Remove unrelated change. --- Objects/complexobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 0576c06b0a9309..a082ba7657e531 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -22,10 +22,10 @@ class complex "PyComplexObject *" "&PyComplex_Type" #include "clinic/complexobject.c.h" -static Py_complex c_1 = {1., 0.}; - /* elementary operations on complex numbers */ +static Py_complex c_1 = {1., 0.}; + Py_complex _Py_c_sum(Py_complex a, Py_complex b) { From e87627317782f0b3b956d15ae4829b16e452bbde Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 12 Dec 2024 13:39:34 +0200 Subject: [PATCH 4/4] Add more tests. --- Lib/test/test_complex.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 242504bb79f01a..3facd38e0f732f 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -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 @@ -456,16 +460,14 @@ def test_pow_with_small_integer_exponents(self): self.assertComplexesAreIdentical(c**1, c) self.assertComplexesAreIdentical(c**2, c*c) self.assertComplexesAreIdentical(c**3, c*(c*c)) - self.assertComplexesAreIdentical(c**4, (c*c)*(c*c)) - self.assertComplexesAreIdentical(c**5, c*((c*c)*(c*c))) - self.assertComplexesAreIdentical(c**6, (c*c)*((c*c)*(c*c))) - self.assertComplexesAreIdentical(c**7, c*(c*c)*((c*c)*(c*c))) - self.assertComplexesAreIdentical(c**8, ((c*c)*(c*c))*((c*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) @@ -485,6 +487,25 @@ def test_pow_with_small_integer_exponents(self): 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))