Skip to content

Commit

Permalink
gh-94906: Support multiple steps in math.nextafter (#103881)
Browse files Browse the repository at this point in the history
This PR updates `math.nextafter` to add a new `steps` argument. The behaviour is as though `math.nextafter` had been called `steps` times in succession.

---------

Co-authored-by: Mark Dickinson <mdickinson@enthought.com>
  • Loading branch information
matthiasgoergens and mdickinson authored May 19, 2023
1 parent c3f43bf commit 6e39fa1
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 18 deletions.
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)

Return the next floating-point value after *x* towards *y*.
Return the floating-point value *steps* steps after *x* towards *y*.

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 @@ -681,6 +681,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;};
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

0 comments on commit 6e39fa1

Please sign in to comment.