Skip to content

gh-133895: correct values of cmath.cosh/sinh in case of overflows - #135603

Open
skirpichev wants to merge 5 commits into
python:mainfrom
skirpichev:fix-cmath-sinh-and-cosh/133895
Open

skirpichev wants to merge 5 commits into
python:mainfrom
skirpichev:fix-cmath-sinh-and-cosh/133895

Conversation

@skirpichev

@skirpichev skirpichev commented Jun 17, 2025 •

Copy link
Copy Markdown
Member

This is a split-off gh-134995.

The C17 standard says (cis(y) is defined as cos(y) + i sin(y)):

  • ccosh(+∞ + i0) returns +∞ + i0.
  • ccosh(+∞ + iy) returns +∞ cis(y), for finite nonzero y. and
  • csinh(+∞ + i0) returns +∞ + i0.
  • csinh(+∞ + iy) returns +∞ cis(y), for positive finite y.

So far values, computed for exceptions, aren't accessible from the pure-Python world, yet we are trying to be correct in other places. The Lib/test/mathdata/cmath_testcases.txt has data points with correct numbers (see cosh0032 and sinh0032).

This is a split-off pythongh-134995.

The C17 standard says (cis(y) is defined as cos(y) + i sin(y)):
* ccosh(+∞ + i0) returns +∞ + i0.
* ccosh(+∞ + iy) returns +∞ cis(y), for finite nonzero y.
and
* csinh(+∞ + i0) returns +∞ + i0.
* csinh(+∞ + iy) returns +∞ cis(y), for positive finite y.

So far values, computed for exceptions, aren't accessible from the
pure-Python world, yet we are trying to be correct in other places.  The
Lib/test/mathdata/cmath_testcases.txt has data points with correct
numbers (see cosh0032 and sinh0032).

Also, use AC magic for the rect() value.
@skirpichev
skirpichev force-pushed the fix-cmath-sinh-and-cosh/133895 branch from 303842c to d5bd0a0 Compare June 17, 2025 06:41
@skirpichev skirpichev changed the title gh-134995: correct values of cmath.cosh/sinh in case of overflows gh-133895: correct values of cmath.cosh/sinh in case of overflows Jun 17, 2025
@skirpichev

Copy link
Copy Markdown
Member Author

CC @serhiy-storchaka

@github-actions

Copy link
Copy Markdown

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

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Apr 26, 2026
@skirpichev skirpichev closed this May 4, 2026
@skirpichev
skirpichev deleted the fix-cmath-sinh-and-cosh/133895 branch May 4, 2026 15:18
@skirpichev
skirpichev restored the fix-cmath-sinh-and-cosh/133895 branch May 23, 2026 07:33
@skirpichev skirpichev reopened this May 23, 2026
@github-actions github-actions Bot removed the stale Stale PR or inactive for long period of time. label May 24, 2026
@skirpichev

Copy link
Copy Markdown
Member Author

CC @picnixz

@picnixz
picnixz self-requested a review May 31, 2026 07:53
@zware

zware commented Jul 18, 2026

Copy link
Copy Markdown
Member

The cmath.rect change appears unrelated to this issue. I would approve and merge that change on its own (no issue required), but I do not have enough background to approve the rest of it (addressing the linked issue :)).

@skirpichev

Copy link
Copy Markdown
Member Author

The cmath.rect change appears unrelated to this issue. I would approve and merge that change on its own (no issue required)

FYI: #154034

@skirpichev

Copy link
Copy Markdown
Member Author

@vstinner, does it make sense for you?

Below attached pure-Python version of the main code and patched version, with tests against libm. Results for unpatched case:

$ ./python a.py 
csinh (720+0j) (inf+0j) (inf+nanj)
ccosh (720+0j) (inf+0j) (inf+nanj)
csinh (720-0j) (inf-0j) (inf+nanj)
ccosh (720-0j) (inf-0j) (inf+nanj)
csinh (-720+0j) (-inf+0j) (-inf+nanj)
ccosh (-720+0j) (inf-0j) (inf+nanj)
csinh (-720-0j) (-inf-0j) (-inf+nanj)
ccosh (-720-0j) (inf+0j) (inf+nanj)
Details
import cmath
import sys
from math import (isinf, isfinite, isnan, copysign, inf,
                  e, log, nan, isclose)
import ctypes
import ctypes.util
libm = ctypes.CDLL('libm.so.6')

@ctypes.util.wrap_dll_function(libm)
def csinh(z: ctypes.c_double_complex) -> ctypes.c_double_complex:
    pass
@ctypes.util.wrap_dll_function(libm)
def ccosh(z: ctypes.c_double_complex) -> ctypes.c_double_complex:
    pass

@ctypes.util.wrap_dll_function(libm)
def sinh(z: ctypes.c_double) -> ctypes.c_double:
    pass
@ctypes.util.wrap_dll_function(libm)
def sin(z: ctypes.c_double) -> ctypes.c_double:
    pass
@ctypes.util.wrap_dll_function(libm)
def cosh(z: ctypes.c_double) -> ctypes.c_double:
    pass
@ctypes.util.wrap_dll_function(libm)
def cos(z: ctypes.c_double) -> ctypes.c_double:
    pass


# Enumeration-like indices mapping to the 7x7 matrix taxonomy
ST_NINF  = 0  # -inf
ST_NEG   = 1  # finite number < 0
ST_NZERO = 2  # -0.0
ST_PZERO = 3  # +0.0
ST_POS   = 4  # finite number > 0
ST_PINF  = 5  # +inf
ST_NAN   = 6  # nan

U = -9.5426319407711027e33  # CPython placeholder

# Matrix rows = special_type(z.real), columns = special_type(z.imag)
SINH_SPECIAL_VALUES = [
    # ST_NINF (real == -inf)
    [(inf,nan), (U,U), (-inf,-0.0), (-inf,0.0), (U,U), (inf,nan), (inf,nan)],
    # ST_NEG (real < 0)
    [(nan,nan), (U,U), (U,U), (U,U), (U,U), (nan,nan),  (nan,nan)],
    # ST_NZERO (real == -0.0)
    [(0.0,nan), (U,U), (-0.0,-0.0), (-0.0,0.0), (U,U), (0.0,nan),  (0.0,nan)],
    # ST_PZERO (real == +0.0)
    [(0.0,nan), (U,U), (0.0,-0.0), (0.0,0.0), (U,U), (0.0,nan), (0.0,nan)],
    # ST_POS (real > 0)
    [(nan,nan), (U,U), (U,U), (U, U), (U,U), (nan,nan), (nan,nan)],
    # ST_PINF (real == +inf)
    [(inf,nan), (U,U), (inf,-0.0), (inf,0.0), (U,U), (inf,nan), (inf,nan)],
    # ST_NAN (real == nan)
    [(nan,nan), (nan,nan), (nan,-0.0), (nan,0.0), (nan,nan), (nan,nan), (nan,nan)]
]

COSH_SPECIAL_VALUES = [
    # ST_NINF (real == -inf)
    [(inf,nan), (U,U), (inf,0.0), (inf,-0.0), (U,U), (inf,nan), (inf,nan)],
    # ST_NEG (real < 0)
    [(nan,nan), (U,U), (U, U), (U, U), (U, U), (nan,nan), (nan,nan)],
    # ST_NZERO (real == -0.0)
    [(nan,0.0), (U, U), (1.0,0.0), (1.0,-0.0), (U, U), (nan,0.0), (nan,0.0)],
    # ST_PZERO (real == +0.0)
    [(nan,0.0), (U, U), (1.0,-0.0), (1.0,0.0), (U, U), (nan,0.0), (nan,0.0)],
    # ST_POS (real > 0)
    [(nan,nan), (U, U), (U, U), (U, U), (U, U), (nan,nan), (nan,nan)],
    # ST_PINF (real == +inf)
    [(inf,nan), (U,U), (inf,-0.0), (inf,0.0), (U,U), (inf,nan), (inf,nan)],
    # ST_NAN (real == nan)
    [(nan,nan), (nan,nan), (nan,0.0), (nan,0.0), (nan,nan), (nan,nan), (nan,nan)]
]


def special_type(x):
    if isfinite(x):
        if x:
            return ST_POS if copysign(1.0, x) == 1.0 else ST_NEG
        else:
            return ST_PZERO if copysign(1.0, x) == 1.0 else ST_NZERO
    elif isnan(x):
        return ST_NAN
    else:
        return ST_PINF if copysign(1.0, x) == 1.0 else ST_NINF


CM_LARGE_DOUBLE = sys.float_info.max/4
CM_LOG_LARGE_DOUBLE = log(CM_LARGE_DOUBLE)


def cis(x):
    return complex(cos(x), sin(x))


def py_sinh(z, patch=0):
    if not cmath.isfinite(z):
        if isinf(z.real) and isfinite(z.imag) and z.imag:
            if z.real > 0:
                return inf*cis(z.imag)
            else:
                return -inf*cis(z.imag).conjugate()

        row = special_type(x)
        col = special_type(y)
        return complex(*SINH_SPECIAL_VALUES[row][col])

    if abs(z.real) > CM_LOG_LARGE_DOUBLE:
        x_minus_one = z.real - copysign(1, z.real)
        r = complex(cos(z.imag) * sinh(x_minus_one),
                    sin(z.imag) * cosh(x_minus_one)) * e
        if patch and isnan(r.imag):
            r = complex(r.real, copysign(0.0, z.imag))
        return r
    else:
        return complex(cos(z.imag) * sinh(z.real),
                       sin(z.imag) * cosh(z.real))


def py_cosh(z, patch=0):
    if not cmath.isfinite(z):
        if isinf(z.real) and isfinite(z.imag) and z.imag:
            if z.real > 0:
                return inf*cis(z.imag)
            else:
                return inf*cis(z.imag).conjugate()

        row = special_type(x)
        col = special_type(y)
        return complex(*COSH_SPECIAL_VALUES[row][col])

    if abs(z.real) > CM_LOG_LARGE_DOUBLE:
        x_minus_one = z.real - copysign(1, z.real)
        r = complex(cos(z.imag) * cosh(x_minus_one),
                    sin(z.imag) * sinh(x_minus_one)) * e
        if patch and isnan(r.imag):
            r = complex(r.real, copysign(0.0, z.real*z.imag))
        return r
    else:
        return complex(cos(z.imag) * cosh(z.real),
                       sin(z.imag) * sinh(z.real))


def test(stmt, f, z, cr, r):
    if not stmt:
        print(f.__name__, z, cr, r)


if __name__ == "__main__":
    cases = [-inf, inf, nan, 0.0, -0.0, 0.5, 0.5,
             720, -720]  # overflows
    patch = 0
    if len(sys.argv) > 1:
        patch = int(sys.argv[1])

    for x in cases:
        for y in cases:
            z = complex(x, y)

            for pf, cf in [(py_sinh, csinh), (py_cosh, ccosh)]:
                r = pf(z, patch)
                cr = cf(z)

                if not cmath.isnan(r):
                    test(isclose(r.real, cr.real), cf, z, cr, r)
                    test(isclose(r.imag, cr.imag), cf, z, cr, r)
                else:
                    if isnan(r.real):
                        test(isnan(cr.real), cf, z, cr, r)
                        if isnan(r.imag):
                            test(isnan(cr.imag), cf, z, cr, r)
                        else:
                            test(isclose(r.imag, cr.imag), cf, z, cr, r)
                    else:
                        test(isnan(cr.imag), cf, z, cr, r)
                        test(isclose(r.real, cr.real), cf, z, cr, r)

@vstinner vstinner 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.

You didn't update test_cmath. Does it mean that modified code is not currently tested? If yes, would it be possible to add tests?

@skirpichev

Copy link
Copy Markdown
Member Author

Does it mean that modified code is not currently tested?

Yes, currently these values aren't accessible from the pure-Python world. See PR description and the issue thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants