Skip to content

Commit

Permalink
pythongh-69639: Add mixed-mode rules for complex arithmetic (C-like) (p…
Browse files Browse the repository at this point in the history
…ythonGH-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).
  • Loading branch information
skirpichev authored Nov 26, 2024
1 parent dcf6292 commit 987311d
Show file tree
Hide file tree
Showing 15 changed files with 444 additions and 93 deletions.
54 changes: 54 additions & 0 deletions Doc/c-api/complex.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,36 @@ pointers. This is consistent throughout the API.
representation.
.. c:function:: Py_complex _Py_cr_sum(Py_complex left, double right)
Return the sum of a complex number and a real number, using the C :c:type:`Py_complex`
representation.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_c_diff(Py_complex left, Py_complex right)
Return the difference between two complex numbers, using the C
:c:type:`Py_complex` representation.
.. c:function:: Py_complex _Py_cr_diff(Py_complex left, double right)
Return the difference between a complex number and a real number, using the C
:c:type:`Py_complex` representation.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_rc_diff(double left, Py_complex right)
Return the difference between a real number and a complex number, using the C
:c:type:`Py_complex` representation.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_c_neg(Py_complex num)
Return the negation of the complex number *num*, using the C
Expand All @@ -62,6 +86,14 @@ pointers. This is consistent throughout the API.
representation.
.. c:function:: Py_complex _Py_cr_prod(Py_complex left, double right)
Return the product of a complex number and a real number, using the C
:c:type:`Py_complex` representation.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_c_quot(Py_complex dividend, Py_complex divisor)
Return the quotient of two complex numbers, using the C :c:type:`Py_complex`
Expand All @@ -71,6 +103,28 @@ pointers. This is consistent throughout the API.
:c:data:`errno` to :c:macro:`!EDOM`.
.. c:function:: Py_complex _Py_cr_quot(Py_complex dividend, double divisor)
Return the quotient of a complex number and a real number, using the C
:c:type:`Py_complex` representation.
If *divisor* is zero, this method returns zero and sets
:c:data:`errno` to :c:macro:`!EDOM`.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_rc_quot(double dividend, Py_complex divisor)
Return the quotient of a real number and a complex number, using the C
:c:type:`Py_complex` representation.
If *divisor* is zero, this method returns zero and sets
:c:data:`errno` to :c:macro:`!EDOM`.
.. versionadded:: 3.14
.. c:function:: Py_complex _Py_c_pow(Py_complex num, Py_complex exp)
Return the exponentiation of *num* by *exp*, using the C :c:type:`Py_complex`
Expand Down
12 changes: 6 additions & 6 deletions Doc/library/cmath.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ the function is then applied to the result of the conversion.
imaginary axis we look at the sign of the real part.

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

>>> cmath.sqrt(complex(-2.0, -0.0))
>>> cmath.sqrt(-2-0j)
-1.4142135623730951j

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

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


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

>>> phase(complex(-1.0, 0.0))
>>> phase(-1+0j)
3.141592653589793
>>> phase(complex(-1.0, -0.0))
>>> phase(-1-0j)
-3.141592653589793


Expand Down
16 changes: 11 additions & 5 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ numeric literal yields an imaginary number (a complex number with a zero real
part) which you can add to an integer or float to get a complex number with real
and imaginary parts.

The constructors :func:`int`, :func:`float`, and
:func:`complex` can be used to produce numbers of a specific type.

.. index::
single: arithmetic
pair: built-in function; int
Expand All @@ -262,12 +265,15 @@ and imaginary parts.

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

The constructors :func:`int`, :func:`float`, and
:func:`complex` can be used to produce numbers of a specific type.
x + complex(u, v) = complex(x + u, v)
x * complex(u, v) = complex(x * u, x * v)

A comparison between numbers of different types behaves as though the exact
values of those numbers were being compared. [2]_

All numeric types (except complex) support the following operations (for priorities of
the operations, see :ref:`operator-summary`):
Expand Down
25 changes: 18 additions & 7 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@ Arithmetic conversions
.. index:: pair: arithmetic; conversion

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

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

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

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

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

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

.. versionchanged:: 3.14
If only one operand is a complex number, the other operand is converted
to a floating-point number.

.. index::
single: matrix multiplication
pair: operator; @ (at)
Expand Down Expand Up @@ -1396,23 +1399,31 @@ floating-point number using the :func:`abs` function if appropriate.

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

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

.. versionchanged:: 3.14
If only one operand is a complex number, the other operand is converted
to a floating-point number.

.. index::
single: subtraction
single: operator; - (minus)
single: - (minus); binary operator

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

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

.. versionchanged:: 3.14
If only one operand is a complex number, the other operand is converted
to a floating-point number.


.. _shifting:

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ Other language changes
They raise an error if the argument is a string.
(Contributed by Serhiy Storchaka in :gh:`84978`.)

* Implement mixed-mode arithmetic rules combining real and complex numbers as
specified by C standards since C99.
(Contributed by Sergey B Kirpichev in :gh:`69639`.)

* All Windows code pages are now supported as "cpXXX" codecs on Windows.
(Contributed by Serhiy Storchaka in :gh:`123803`.)

Expand Down
6 changes: 6 additions & 0 deletions Include/cpython/complexobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ typedef struct {

// Operations on complex numbers.
PyAPI_FUNC(Py_complex) _Py_c_sum(Py_complex, Py_complex);
PyAPI_FUNC(Py_complex) _Py_cr_sum(Py_complex, double);
PyAPI_FUNC(Py_complex) _Py_c_diff(Py_complex, Py_complex);
PyAPI_FUNC(Py_complex) _Py_cr_diff(Py_complex, double);
PyAPI_FUNC(Py_complex) _Py_rc_diff(double, Py_complex);
PyAPI_FUNC(Py_complex) _Py_c_neg(Py_complex);
PyAPI_FUNC(Py_complex) _Py_c_prod(Py_complex, Py_complex);
PyAPI_FUNC(Py_complex) _Py_cr_prod(Py_complex, double);
PyAPI_FUNC(Py_complex) _Py_c_quot(Py_complex, Py_complex);
PyAPI_FUNC(Py_complex) _Py_cr_quot(Py_complex, double);
PyAPI_FUNC(Py_complex) _Py_rc_quot(double, Py_complex);
PyAPI_FUNC(Py_complex) _Py_c_pow(Py_complex, Py_complex);
PyAPI_FUNC(double) _Py_c_abs(Py_complex);

Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_floatobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ extern PyObject* _Py_string_to_number_with_underscores(

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

extern int _Py_convert_int_to_double(PyObject **v, double *dbl);


#ifdef __cplusplus
}
Expand Down
14 changes: 13 additions & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from test.support.import_helper import import_module
from test.support.os_helper import (EnvironmentVarGuard, TESTFN, unlink)
from test.support.script_helper import assert_python_ok
from test.support.testcase import ComplexesAreIdenticalMixin
from test.support.warnings_helper import check_warnings
from test.support import requires_IEEE_754
from unittest.mock import MagicMock, patch
Expand Down Expand Up @@ -151,7 +152,7 @@ def map_char(arg):
def pack(*args):
return args

class BuiltinTest(unittest.TestCase):
class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
# Helper to check picklability
def check_iter_pickle(self, it, seq, proto):
itorg = it
Expand Down Expand Up @@ -1902,6 +1903,17 @@ def __getitem__(self, index):
self.assertEqual(sum(xs), complex(sum(z.real for z in xs),
sum(z.imag for z in xs)))

# test that sum() of complex and real numbers doesn't
# smash sign of imaginary 0
self.assertComplexesAreIdentical(sum([complex(1, -0.0), 1]),
complex(2, -0.0))
self.assertComplexesAreIdentical(sum([1, complex(1, -0.0)]),
complex(2, -0.0))
self.assertComplexesAreIdentical(sum([complex(1, -0.0), 1.0]),
complex(2, -0.0))
self.assertComplexesAreIdentical(sum([1.0, complex(1, -0.0)]),
complex(2, -0.0))

@requires_IEEE_754
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
"sum accuracy not guaranteed on machines with double rounding")
Expand Down
45 changes: 44 additions & 1 deletion Lib/test/test_capi/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
FloatSubclass, Float, BadFloat,
BadFloat2, ComplexSubclass)
from test.support import import_helper
from test.support.testcase import ComplexesAreIdenticalMixin


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


class CAPIComplexTest(unittest.TestCase):
class CAPIComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase):
def test_check(self):
# Test PyComplex_Check()
check = _testlimitedcapi.complex_check
Expand Down Expand Up @@ -171,12 +172,33 @@ def test_py_c_sum(self):

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

def test_py_cr_sum(self):
# Test _Py_cr_sum()
_py_cr_sum = _testcapi._py_cr_sum

self.assertComplexesAreIdentical(_py_cr_sum(-0j, -0.0)[0],
complex(-0.0, -0.0))

def test_py_c_diff(self):
# Test _Py_c_diff()
_py_c_diff = _testcapi._py_c_diff

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

def test_py_cr_diff(self):
# Test _Py_cr_diff()
_py_cr_diff = _testcapi._py_cr_diff

self.assertComplexesAreIdentical(_py_cr_diff(-0j, 0.0)[0],
complex(-0.0, -0.0))

def test_py_rc_diff(self):
# Test _Py_rc_diff()
_py_rc_diff = _testcapi._py_rc_diff

self.assertComplexesAreIdentical(_py_rc_diff(-0.0, 0j)[0],
complex(-0.0, -0.0))

def test_py_c_neg(self):
# Test _Py_c_neg()
_py_c_neg = _testcapi._py_c_neg
Expand All @@ -189,6 +211,13 @@ def test_py_c_prod(self):

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

def test_py_cr_prod(self):
# Test _Py_cr_prod()
_py_cr_prod = _testcapi._py_cr_prod

self.assertComplexesAreIdentical(_py_cr_prod(complex('inf+1j'), INF)[0],
complex('inf+infj'))

def test_py_c_quot(self):
# Test _Py_c_quot()
_py_c_quot = _testcapi._py_c_quot
Expand All @@ -211,6 +240,20 @@ def test_py_c_quot(self):

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

def test_py_cr_quot(self):
# Test _Py_cr_quot()
_py_cr_quot = _testcapi._py_cr_quot

self.assertComplexesAreIdentical(_py_cr_quot(complex('inf+1j'), 2**1000)[0],
INF + 2**-1000*1j)

def test_py_rc_quot(self):
# Test _Py_rc_quot()
_py_rc_quot = _testcapi._py_rc_quot

self.assertComplexesAreIdentical(_py_rc_quot(1.0, complex('nan-infj'))[0],
0j)

def test_py_c_pow(self):
# Test _Py_c_pow()
_py_c_pow = _testcapi._py_c_pow
Expand Down
Loading

0 comments on commit 987311d

Please sign in to comment.