Skip to content

Commit 987311d

Browse files
authored
pythongh-69639: Add mixed-mode rules for complex arithmetic (C-like) (pythonGH-124829)
"Generally, mixed-mode arithmetic combining real and complex variables should be performed directly, not by first coercing the real to complex, lest the sign of zero be rendered uninformative; the same goes for combinations of pure imaginary quantities with complex variables." (c) Kahan, W: Branch cuts for complex elementary functions. This patch implements mixed-mode arithmetic rules, combining real and complex variables as specified by C standards since C99 (in particular, there is no special version for the true division with real lhs operand). Most C compilers implementing C99+ Annex G have only these special rules (without support for imaginary type, which is going to be deprecated in C2y).
1 parent dcf6292 commit 987311d

File tree

15 files changed

+444
-93
lines changed

15 files changed

+444
-93
lines changed

Doc/c-api/complex.rst

+54
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,36 @@ pointers. This is consistent throughout the API.
4444
representation.
4545
4646
47+
.. c:function:: Py_complex _Py_cr_sum(Py_complex left, double right)
48+
49+
Return the sum of a complex number and a real number, using the C :c:type:`Py_complex`
50+
representation.
51+
52+
.. versionadded:: 3.14
53+
54+
4755
.. c:function:: Py_complex _Py_c_diff(Py_complex left, Py_complex right)
4856
4957
Return the difference between two complex numbers, using the C
5058
:c:type:`Py_complex` representation.
5159
5260
61+
.. c:function:: Py_complex _Py_cr_diff(Py_complex left, double right)
62+
63+
Return the difference between a complex number and a real number, using the C
64+
:c:type:`Py_complex` representation.
65+
66+
.. versionadded:: 3.14
67+
68+
69+
.. c:function:: Py_complex _Py_rc_diff(double left, Py_complex right)
70+
71+
Return the difference between a real number and a complex number, using the C
72+
:c:type:`Py_complex` representation.
73+
74+
.. versionadded:: 3.14
75+
76+
5377
.. c:function:: Py_complex _Py_c_neg(Py_complex num)
5478
5579
Return the negation of the complex number *num*, using the C
@@ -62,6 +86,14 @@ pointers. This is consistent throughout the API.
6286
representation.
6387
6488
89+
.. c:function:: Py_complex _Py_cr_prod(Py_complex left, double right)
90+
91+
Return the product of a complex number and a real number, using the C
92+
:c:type:`Py_complex` representation.
93+
94+
.. versionadded:: 3.14
95+
96+
6597
.. c:function:: Py_complex _Py_c_quot(Py_complex dividend, Py_complex divisor)
6698
6799
Return the quotient of two complex numbers, using the C :c:type:`Py_complex`
@@ -71,6 +103,28 @@ pointers. This is consistent throughout the API.
71103
:c:data:`errno` to :c:macro:`!EDOM`.
72104
73105
106+
.. c:function:: Py_complex _Py_cr_quot(Py_complex dividend, double divisor)
107+
108+
Return the quotient of a complex number and a real number, using the C
109+
:c:type:`Py_complex` representation.
110+
111+
If *divisor* is zero, this method returns zero and sets
112+
:c:data:`errno` to :c:macro:`!EDOM`.
113+
114+
.. versionadded:: 3.14
115+
116+
117+
.. c:function:: Py_complex _Py_rc_quot(double dividend, Py_complex divisor)
118+
119+
Return the quotient of a real number and a complex number, using the C
120+
:c:type:`Py_complex` representation.
121+
122+
If *divisor* is zero, this method returns zero and sets
123+
:c:data:`errno` to :c:macro:`!EDOM`.
124+
125+
.. versionadded:: 3.14
126+
127+
74128
.. c:function:: Py_complex _Py_c_pow(Py_complex num, Py_complex exp)
75129
76130
Return the exponentiation of *num* by *exp*, using the C :c:type:`Py_complex`

Doc/library/cmath.rst

+6-6
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ the function is then applied to the result of the conversion.
2424
imaginary axis we look at the sign of the real part.
2525

2626
For example, the :func:`cmath.sqrt` function has a branch cut along the
27-
negative real axis. An argument of ``complex(-2.0, -0.0)`` is treated as
27+
negative real axis. An argument of ``-2-0j`` is treated as
2828
though it lies *below* the branch cut, and so gives a result on the negative
2929
imaginary axis::
3030

31-
>>> cmath.sqrt(complex(-2.0, -0.0))
31+
>>> cmath.sqrt(-2-0j)
3232
-1.4142135623730951j
3333

34-
But an argument of ``complex(-2.0, 0.0)`` is treated as though it lies above
34+
But an argument of ``-2+0j`` is treated as though it lies above
3535
the branch cut::
3636

37-
>>> cmath.sqrt(complex(-2.0, 0.0))
37+
>>> cmath.sqrt(-2+0j)
3838
1.4142135623730951j
3939

4040

@@ -63,9 +63,9 @@ rectangular coordinates to polar coordinates and back.
6363
along the negative real axis. The sign of the result is the same as the
6464
sign of ``x.imag``, even when ``x.imag`` is zero::
6565

66-
>>> phase(complex(-1.0, 0.0))
66+
>>> phase(-1+0j)
6767
3.141592653589793
68-
>>> phase(complex(-1.0, -0.0))
68+
>>> phase(-1-0j)
6969
-3.141592653589793
7070

7171

Doc/library/stdtypes.rst

+11-5
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ numeric literal yields an imaginary number (a complex number with a zero real
243243
part) which you can add to an integer or float to get a complex number with real
244244
and imaginary parts.
245245

246+
The constructors :func:`int`, :func:`float`, and
247+
:func:`complex` can be used to produce numbers of a specific type.
248+
246249
.. index::
247250
single: arithmetic
248251
pair: built-in function; int
@@ -262,12 +265,15 @@ and imaginary parts.
262265

263266
Python fully supports mixed arithmetic: when a binary arithmetic operator has
264267
operands of different numeric types, the operand with the "narrower" type is
265-
widened to that of the other, where integer is narrower than floating point,
266-
which is narrower than complex. A comparison between numbers of different types
267-
behaves as though the exact values of those numbers were being compared. [2]_
268+
widened to that of the other, where integer is narrower than floating point.
269+
Arithmetic with complex and real operands is defined by the usual mathematical
270+
formula, for example::
268271

269-
The constructors :func:`int`, :func:`float`, and
270-
:func:`complex` can be used to produce numbers of a specific type.
272+
x + complex(u, v) = complex(x + u, v)
273+
x * complex(u, v) = complex(x * u, x * v)
274+
275+
A comparison between numbers of different types behaves as though the exact
276+
values of those numbers were being compared. [2]_
271277

272278
All numeric types (except complex) support the following operations (for priorities of
273279
the operations, see :ref:`operator-summary`):

Doc/reference/expressions.rst

+18-7
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ Arithmetic conversions
2828
.. index:: pair: arithmetic; conversion
2929

3030
When a description of an arithmetic operator below uses the phrase "the numeric
31-
arguments are converted to a common type", this means that the operator
31+
arguments are converted to a common real type", this means that the operator
3232
implementation for built-in types works as follows:
3333

34-
* If either argument is a complex number, the other is converted to complex;
34+
* If both arguments are complex numbers, no conversion is performed;
3535

36-
* otherwise, if either argument is a floating-point number, the other is
37-
converted to floating point;
36+
* if either argument is a complex or a floating-point number, the other is converted to a floating-point number;
3837

3938
* otherwise, both must be integers and no conversion is necessary.
4039

@@ -1323,12 +1322,16 @@ operators and one for additive operators:
13231322
The ``*`` (multiplication) operator yields the product of its arguments. The
13241323
arguments must either both be numbers, or one argument must be an integer and
13251324
the other must be a sequence. In the former case, the numbers are converted to a
1326-
common type and then multiplied together. In the latter case, sequence
1325+
common real type and then multiplied together. In the latter case, sequence
13271326
repetition is performed; a negative repetition factor yields an empty sequence.
13281327

13291328
This operation can be customized using the special :meth:`~object.__mul__` and
13301329
:meth:`~object.__rmul__` methods.
13311330

1331+
.. versionchanged:: 3.14
1332+
If only one operand is a complex number, the other operand is converted
1333+
to a floating-point number.
1334+
13321335
.. index::
13331336
single: matrix multiplication
13341337
pair: operator; @ (at)
@@ -1396,23 +1399,31 @@ floating-point number using the :func:`abs` function if appropriate.
13961399

13971400
The ``+`` (addition) operator yields the sum of its arguments. The arguments
13981401
must either both be numbers or both be sequences of the same type. In the
1399-
former case, the numbers are converted to a common type and then added together.
1402+
former case, the numbers are converted to a common real type and then added together.
14001403
In the latter case, the sequences are concatenated.
14011404

14021405
This operation can be customized using the special :meth:`~object.__add__` and
14031406
:meth:`~object.__radd__` methods.
14041407

1408+
.. versionchanged:: 3.14
1409+
If only one operand is a complex number, the other operand is converted
1410+
to a floating-point number.
1411+
14051412
.. index::
14061413
single: subtraction
14071414
single: operator; - (minus)
14081415
single: - (minus); binary operator
14091416

14101417
The ``-`` (subtraction) operator yields the difference of its arguments. The
1411-
numeric arguments are first converted to a common type.
1418+
numeric arguments are first converted to a common real type.
14121419

14131420
This operation can be customized using the special :meth:`~object.__sub__` and
14141421
:meth:`~object.__rsub__` methods.
14151422

1423+
.. versionchanged:: 3.14
1424+
If only one operand is a complex number, the other operand is converted
1425+
to a floating-point number.
1426+
14161427

14171428
.. _shifting:
14181429

Doc/whatsnew/3.14.rst

+4
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ Other language changes
230230
They raise an error if the argument is a string.
231231
(Contributed by Serhiy Storchaka in :gh:`84978`.)
232232

233+
* Implement mixed-mode arithmetic rules combining real and complex numbers as
234+
specified by C standards since C99.
235+
(Contributed by Sergey B Kirpichev in :gh:`69639`.)
236+
233237
* All Windows code pages are now supported as "cpXXX" codecs on Windows.
234238
(Contributed by Serhiy Storchaka in :gh:`123803`.)
235239

Include/cpython/complexobject.h

+6
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ typedef struct {
99

1010
// Operations on complex numbers.
1111
PyAPI_FUNC(Py_complex) _Py_c_sum(Py_complex, Py_complex);
12+
PyAPI_FUNC(Py_complex) _Py_cr_sum(Py_complex, double);
1213
PyAPI_FUNC(Py_complex) _Py_c_diff(Py_complex, Py_complex);
14+
PyAPI_FUNC(Py_complex) _Py_cr_diff(Py_complex, double);
15+
PyAPI_FUNC(Py_complex) _Py_rc_diff(double, Py_complex);
1316
PyAPI_FUNC(Py_complex) _Py_c_neg(Py_complex);
1417
PyAPI_FUNC(Py_complex) _Py_c_prod(Py_complex, Py_complex);
18+
PyAPI_FUNC(Py_complex) _Py_cr_prod(Py_complex, double);
1519
PyAPI_FUNC(Py_complex) _Py_c_quot(Py_complex, Py_complex);
20+
PyAPI_FUNC(Py_complex) _Py_cr_quot(Py_complex, double);
21+
PyAPI_FUNC(Py_complex) _Py_rc_quot(double, Py_complex);
1622
PyAPI_FUNC(Py_complex) _Py_c_pow(Py_complex, Py_complex);
1723
PyAPI_FUNC(double) _Py_c_abs(Py_complex);
1824

Include/internal/pycore_floatobject.h

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ extern PyObject* _Py_string_to_number_with_underscores(
5454

5555
extern double _Py_parse_inf_or_nan(const char *p, char **endptr);
5656

57+
extern int _Py_convert_int_to_double(PyObject **v, double *dbl);
58+
5759

5860
#ifdef __cplusplus
5961
}

Lib/test/test_builtin.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from test.support.import_helper import import_module
3636
from test.support.os_helper import (EnvironmentVarGuard, TESTFN, unlink)
3737
from test.support.script_helper import assert_python_ok
38+
from test.support.testcase import ComplexesAreIdenticalMixin
3839
from test.support.warnings_helper import check_warnings
3940
from test.support import requires_IEEE_754
4041
from unittest.mock import MagicMock, patch
@@ -151,7 +152,7 @@ def map_char(arg):
151152
def pack(*args):
152153
return args
153154

154-
class BuiltinTest(unittest.TestCase):
155+
class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
155156
# Helper to check picklability
156157
def check_iter_pickle(self, it, seq, proto):
157158
itorg = it
@@ -1902,6 +1903,17 @@ def __getitem__(self, index):
19021903
self.assertEqual(sum(xs), complex(sum(z.real for z in xs),
19031904
sum(z.imag for z in xs)))
19041905

1906+
# test that sum() of complex and real numbers doesn't
1907+
# smash sign of imaginary 0
1908+
self.assertComplexesAreIdentical(sum([complex(1, -0.0), 1]),
1909+
complex(2, -0.0))
1910+
self.assertComplexesAreIdentical(sum([1, complex(1, -0.0)]),
1911+
complex(2, -0.0))
1912+
self.assertComplexesAreIdentical(sum([complex(1, -0.0), 1.0]),
1913+
complex(2, -0.0))
1914+
self.assertComplexesAreIdentical(sum([1.0, complex(1, -0.0)]),
1915+
complex(2, -0.0))
1916+
19051917
@requires_IEEE_754
19061918
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
19071919
"sum accuracy not guaranteed on machines with double rounding")

Lib/test/test_capi/test_complex.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
FloatSubclass, Float, BadFloat,
88
BadFloat2, ComplexSubclass)
99
from test.support import import_helper
10+
from test.support.testcase import ComplexesAreIdenticalMixin
1011

1112

1213
_testcapi = import_helper.import_module('_testcapi')
@@ -23,7 +24,7 @@ def __complex__(self):
2324
raise RuntimeError
2425

2526

26-
class CAPIComplexTest(unittest.TestCase):
27+
class CAPIComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase):
2728
def test_check(self):
2829
# Test PyComplex_Check()
2930
check = _testlimitedcapi.complex_check
@@ -171,12 +172,33 @@ def test_py_c_sum(self):
171172

172173
self.assertEqual(_py_c_sum(1, 1j), (1+1j, 0))
173174

175+
def test_py_cr_sum(self):
176+
# Test _Py_cr_sum()
177+
_py_cr_sum = _testcapi._py_cr_sum
178+
179+
self.assertComplexesAreIdentical(_py_cr_sum(-0j, -0.0)[0],
180+
complex(-0.0, -0.0))
181+
174182
def test_py_c_diff(self):
175183
# Test _Py_c_diff()
176184
_py_c_diff = _testcapi._py_c_diff
177185

178186
self.assertEqual(_py_c_diff(1, 1j), (1-1j, 0))
179187

188+
def test_py_cr_diff(self):
189+
# Test _Py_cr_diff()
190+
_py_cr_diff = _testcapi._py_cr_diff
191+
192+
self.assertComplexesAreIdentical(_py_cr_diff(-0j, 0.0)[0],
193+
complex(-0.0, -0.0))
194+
195+
def test_py_rc_diff(self):
196+
# Test _Py_rc_diff()
197+
_py_rc_diff = _testcapi._py_rc_diff
198+
199+
self.assertComplexesAreIdentical(_py_rc_diff(-0.0, 0j)[0],
200+
complex(-0.0, -0.0))
201+
180202
def test_py_c_neg(self):
181203
# Test _Py_c_neg()
182204
_py_c_neg = _testcapi._py_c_neg
@@ -189,6 +211,13 @@ def test_py_c_prod(self):
189211

190212
self.assertEqual(_py_c_prod(2, 1j), (2j, 0))
191213

214+
def test_py_cr_prod(self):
215+
# Test _Py_cr_prod()
216+
_py_cr_prod = _testcapi._py_cr_prod
217+
218+
self.assertComplexesAreIdentical(_py_cr_prod(complex('inf+1j'), INF)[0],
219+
complex('inf+infj'))
220+
192221
def test_py_c_quot(self):
193222
# Test _Py_c_quot()
194223
_py_c_quot = _testcapi._py_c_quot
@@ -211,6 +240,20 @@ def test_py_c_quot(self):
211240

212241
self.assertEqual(_py_c_quot(1, 0j)[1], errno.EDOM)
213242

243+
def test_py_cr_quot(self):
244+
# Test _Py_cr_quot()
245+
_py_cr_quot = _testcapi._py_cr_quot
246+
247+
self.assertComplexesAreIdentical(_py_cr_quot(complex('inf+1j'), 2**1000)[0],
248+
INF + 2**-1000*1j)
249+
250+
def test_py_rc_quot(self):
251+
# Test _Py_rc_quot()
252+
_py_rc_quot = _testcapi._py_rc_quot
253+
254+
self.assertComplexesAreIdentical(_py_rc_quot(1.0, complex('nan-infj'))[0],
255+
0j)
256+
214257
def test_py_c_pow(self):
215258
# Test _Py_c_pow()
216259
_py_c_pow = _testcapi._py_c_pow

0 commit comments

Comments
 (0)