Skip to content
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

gh-94906: Support multiple steps in math.nextafter #103881

Merged
merged 23 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
77fb4ae
pythongh-94906: Support multiple steps in math.nextafter
matthiasgoergens Apr 26, 2023
5e97978
Merge branch 'main' into gh-94906
matthiasgoergens May 2, 2023
0f2b30f
Merge remote-tracking branch 'origin/main' into gh-94906
matthiasgoergens May 17, 2023
b90e86c
Pass through NaN unchanged
matthiasgoergens May 17, 2023
accc22f
Explain that we need same endianness
matthiasgoergens May 17, 2023
8e1db39
Allow arbitrarily many steps
matthiasgoergens May 17, 2023
6873347
Fix problems
matthiasgoergens May 17, 2023
1fff8b4
Update regen-all
matthiasgoergens May 17, 2023
311f266
Make steps also available as a non-kw arg
matthiasgoergens May 17, 2023
cd70fea
Fix docs
matthiasgoergens May 17, 2023
e0421b6
Update docs
matthiasgoergens May 17, 2023
a8c83a7
Py_None instead of NULL
matthiasgoergens May 17, 2023
88c46c4
Fix typo
matthiasgoergens May 17, 2023
2232bb4
Tickle the CI/CD
matthiasgoergens May 17, 2023
a2724f6
Revert "Make steps also available as a non-kw arg"
matthiasgoergens May 17, 2023
b7b5ac6
Merge branch 'main' into gh-94906
matthiasgoergens May 17, 2023
b45f9c2
property based tests (should fail)
matthiasgoergens May 17, 2023
1f9d890
Property based tests
matthiasgoergens May 17, 2023
2153302
simplify
matthiasgoergens May 17, 2023
61c7c6e
Tickle CI/CD
matthiasgoergens May 17, 2023
04af22b
Merge branch 'main' into gh-94906
matthiasgoergens May 18, 2023
6527b77
Fix whitespace issue
mdickinson May 19, 2023
66ef3f9
Portability fix and a minor style fix
mdickinson May 19, 2023
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
9 changes: 6 additions & 3 deletions Doc/library/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,11 @@ Number-theoretic and representation functions
of *x* and are floats.


.. function:: nextafter(x, y)
.. function:: nextafter(x, y, steps=1)
terryjreedy marked this conversation as resolved.
Show resolved Hide resolved

Return the next floating-point value after *x* towards *y*.
Return the floating-point value *steps* steps after *x* towards *y*.
matthiasgoergens marked this conversation as resolved.
Show resolved Hide resolved

If *x* is equal to *y*, return *y*.
If *x* is equal to *y*, return *y*, unless *steps* is zero.

Examples:

Expand All @@ -239,6 +239,9 @@ Number-theoretic and representation functions

See also :func:`math.ulp`.

.. versionchanged:: 3.12
Added the *steps* argument.

.. versionadded:: 3.9

.. function:: perm(n, k=None)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(stdin)
STRUCT_FOR_ID(stdout)
STRUCT_FOR_ID(step)
STRUCT_FOR_ID(steps)
STRUCT_FOR_ID(store_name)
STRUCT_FOR_ID(strategy)
STRUCT_FOR_ID(strftime)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -2296,11 +2296,20 @@ def test_nextafter(self):
float.fromhex('0x1.fffffffffffffp-1'))
self.assertEqual(math.nextafter(1.0, INF),
float.fromhex('0x1.0000000000001p+0'))
self.assertEqual(math.nextafter(1.0, -INF, steps=1),
float.fromhex('0x1.fffffffffffffp-1'))
self.assertEqual(math.nextafter(1.0, INF, steps=1),
float.fromhex('0x1.0000000000001p+0'))
self.assertEqual(math.nextafter(1.0, -INF, steps=3),
float.fromhex('0x1.ffffffffffffdp-1'))
self.assertEqual(math.nextafter(1.0, INF, steps=3),
float.fromhex('0x1.0000000000003p+0'))

# x == y: y is returned
self.assertEqual(math.nextafter(2.0, 2.0), 2.0)
self.assertEqualSign(math.nextafter(-0.0, +0.0), +0.0)
self.assertEqualSign(math.nextafter(+0.0, -0.0), -0.0)
for steps in range(1, 5):
self.assertEqual(math.nextafter(2.0, 2.0, steps=steps), 2.0)
self.assertEqualSign(math.nextafter(-0.0, +0.0, steps=steps), +0.0)
self.assertEqualSign(math.nextafter(+0.0, -0.0, steps=steps), -0.0)

# around 0.0
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
Expand All @@ -2325,6 +2334,11 @@ def test_nextafter(self):
self.assertIsNaN(math.nextafter(1.0, NAN))
self.assertIsNaN(math.nextafter(NAN, NAN))

self.assertEqual(1.0, math.nextafter(1.0, INF, steps=0))
with self.assertRaises(ValueError):
math.nextafter(1.0, INF, steps=-1)


@requires_IEEE_754
def test_ulp(self):
self.assertEqual(math.ulp(1.0), sys.float_info.epsilon)
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_math_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import functools
import unittest
from math import isnan, nextafter
from test.support import requires_IEEE_754
from test.support.hypothesis_helper import hypothesis

floats = hypothesis.strategies.floats
integers = hypothesis.strategies.integers


def assert_equal_float(x, y):
assert isnan(x) and isnan(y) or x == y


def via_reduce(x, y, steps):
return functools.reduce(nextafter, [y] * steps, x)


class NextafterTests(unittest.TestCase):
@requires_IEEE_754
@hypothesis.given(
x=floats(),
y=floats(),
steps=integers(min_value=0, max_value=2**16))
def test_count(self, x, y, steps):
assert_equal_float(via_reduce(x, y, steps),
nextafter(x, y, steps=steps))

@requires_IEEE_754
@hypothesis.given(
x=floats(),
y=floats(),
a=integers(min_value=0),
b=integers(min_value=0))
def test_addition_commutes(self, x, y, a, b):
first = nextafter(x, y, steps=a)
second = nextafter(first, y, steps=b)
combined = nextafter(x, y, steps=a+b)
hypothesis.note(f"{first} -> {second} == {combined}")

assert_equal_float(second, combined)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support multiple steps in :func:`math.nextafter`. Patch by Shantanu Jain and Matthias Gorgens.
55 changes: 47 additions & 8 deletions Modules/clinic/mathmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 105 additions & 4 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3864,13 +3864,20 @@ math.nextafter
x: double
y: double
/
*
steps: object = None

Return the floating-point value the given number of steps after x towards y.

If steps is not specified or is None, it defaults to 1.

Return the next floating-point value after x towards y.
Raises a TypeError, if x or y is not a double, or if steps is not an integer.
Raises ValueError if steps is negative.
[clinic start generated code]*/

static PyObject *
math_nextafter_impl(PyObject *module, double x, double y)
/*[clinic end generated code: output=750c8266c1c540ce input=02b2d50cd1d9f9b6]*/
math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps)
/*[clinic end generated code: output=cc6511f02afc099e input=7f2a5842112af2b4]*/
{
#if defined(_AIX)
if (x == y) {
Expand All @@ -3885,7 +3892,101 @@ math_nextafter_impl(PyObject *module, double x, double y)
return PyFloat_FromDouble(y);
}
#endif
return PyFloat_FromDouble(nextafter(x, y));
if (steps == Py_None) {
// fast path: we default to one step.
return PyFloat_FromDouble(nextafter(x, y));
}
steps = PyNumber_Index(steps);
if (steps == NULL) {
return NULL;
}
assert(PyLong_CheckExact(steps));
if (_PyLong_IsNegative((PyLongObject *)steps)) {
PyErr_SetString(PyExc_ValueError,
"steps must be a non-negative integer");
Py_DECREF(steps);
return NULL;
}

unsigned long long usteps_ull = PyLong_AsUnsignedLongLong(steps);
// Conveniently, uint64_t and double have the same number of bits
// on all the platforms we care about.
// So if an overflow occurs, we can just use UINT64_MAX.
Py_DECREF(steps);
if (usteps_ull >= UINT64_MAX) {
// This branch includes the case where an error occurred, since
// (unsigned long long)(-1) = ULLONG_MAX >= UINT64_MAX. Note that
// usteps_ull can be strictly larger than UINT64_MAX on a machine
// where unsigned long long has width > 64 bits.
if (PyErr_Occurred()) {
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
PyErr_Clear();
}
else {
return NULL;
}
}
usteps_ull = UINT64_MAX;
}
assert(usteps_ull <= UINT64_MAX);
uint64_t usteps = (uint64_t)usteps_ull;

if (usteps == 0) {
return PyFloat_FromDouble(x);
}
if (Py_IS_NAN(x)) {
return PyFloat_FromDouble(x);
}
if (Py_IS_NAN(y)) {
return PyFloat_FromDouble(y);
}

// We assume that double and uint64_t have the same endianness.
// This is not guaranteed by the C-standard, but it is true for
// all platforms we care about. (The most likely form of violation
// would be a "mixed-endian" double.)
union pun {double f; uint64_t i;};
matthiasgoergens marked this conversation as resolved.
Show resolved Hide resolved
union pun ux = {x}, uy = {y};
if (ux.i == uy.i) {
return PyFloat_FromDouble(x);
}

const uint64_t sign_bit = 1ULL<<63;

uint64_t ax = ux.i & ~sign_bit;
uint64_t ay = uy.i & ~sign_bit;

// opposite signs
if (((ux.i ^ uy.i) & sign_bit)) {
// NOTE: ax + ay can never overflow, because their most significant bit
// ain't set.
if (ax + ay <= usteps) {
return PyFloat_FromDouble(uy.f);
// This comparison has to use <, because <= would get +0.0 vs -0.0
// wrong.
} else if (ax < usteps) {
union pun result = {.i = (uy.i & sign_bit) | (usteps - ax)};
return PyFloat_FromDouble(result.f);
} else {
ux.i -= usteps;
return PyFloat_FromDouble(ux.f);
}
// same sign
} else if (ax > ay) {
if (ax - ay >= usteps) {
ux.i -= usteps;
return PyFloat_FromDouble(ux.f);
} else {
return PyFloat_FromDouble(uy.f);
}
} else {
if (ay - ax >= usteps) {
ux.i += usteps;
return PyFloat_FromDouble(ux.f);
} else {
return PyFloat_FromDouble(uy.f);
}
}
}


Expand Down