gh-94906: Support multiple steps in math.nextafter (#103881)
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>
This commit is contained in:
parent
c3f43bfb4b
commit
6e39fa1955
@ -224,11 +224,11 @@ Number-theoretic and representation functions
|
|||||||
of *x* and are floats.
|
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:
|
Examples:
|
||||||
|
|
||||||
@ -239,6 +239,9 @@ Number-theoretic and representation functions
|
|||||||
|
|
||||||
See also :func:`math.ulp`.
|
See also :func:`math.ulp`.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.12
|
||||||
|
Added the *steps* argument.
|
||||||
|
|
||||||
.. versionadded:: 3.9
|
.. versionadded:: 3.9
|
||||||
|
|
||||||
.. function:: perm(n, k=None)
|
.. function:: perm(n, k=None)
|
||||||
|
@ -1193,6 +1193,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
|
|||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdin));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdin));
|
||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdout));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdout));
|
||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(step));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(step));
|
||||||
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(steps));
|
||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(store_name));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(store_name));
|
||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strategy));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strategy));
|
||||||
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strftime));
|
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strftime));
|
||||||
|
@ -681,6 +681,7 @@ struct _Py_global_strings {
|
|||||||
STRUCT_FOR_ID(stdin)
|
STRUCT_FOR_ID(stdin)
|
||||||
STRUCT_FOR_ID(stdout)
|
STRUCT_FOR_ID(stdout)
|
||||||
STRUCT_FOR_ID(step)
|
STRUCT_FOR_ID(step)
|
||||||
|
STRUCT_FOR_ID(steps)
|
||||||
STRUCT_FOR_ID(store_name)
|
STRUCT_FOR_ID(store_name)
|
||||||
STRUCT_FOR_ID(strategy)
|
STRUCT_FOR_ID(strategy)
|
||||||
STRUCT_FOR_ID(strftime)
|
STRUCT_FOR_ID(strftime)
|
||||||
|
1
Include/internal/pycore_runtime_init_generated.h
generated
1
Include/internal/pycore_runtime_init_generated.h
generated
@ -1187,6 +1187,7 @@ extern "C" {
|
|||||||
INIT_ID(stdin), \
|
INIT_ID(stdin), \
|
||||||
INIT_ID(stdout), \
|
INIT_ID(stdout), \
|
||||||
INIT_ID(step), \
|
INIT_ID(step), \
|
||||||
|
INIT_ID(steps), \
|
||||||
INIT_ID(store_name), \
|
INIT_ID(store_name), \
|
||||||
INIT_ID(strategy), \
|
INIT_ID(strategy), \
|
||||||
INIT_ID(strftime), \
|
INIT_ID(strftime), \
|
||||||
|
@ -1884,6 +1884,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
|
|||||||
string = &_Py_ID(step);
|
string = &_Py_ID(step);
|
||||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||||
_PyUnicode_InternInPlace(interp, &string);
|
_PyUnicode_InternInPlace(interp, &string);
|
||||||
|
string = &_Py_ID(steps);
|
||||||
|
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||||
|
_PyUnicode_InternInPlace(interp, &string);
|
||||||
string = &_Py_ID(store_name);
|
string = &_Py_ID(store_name);
|
||||||
assert(_PyUnicode_CheckConsistency(string, 1));
|
assert(_PyUnicode_CheckConsistency(string, 1));
|
||||||
_PyUnicode_InternInPlace(interp, &string);
|
_PyUnicode_InternInPlace(interp, &string);
|
||||||
|
@ -2296,11 +2296,20 @@ class MathTests(unittest.TestCase):
|
|||||||
float.fromhex('0x1.fffffffffffffp-1'))
|
float.fromhex('0x1.fffffffffffffp-1'))
|
||||||
self.assertEqual(math.nextafter(1.0, INF),
|
self.assertEqual(math.nextafter(1.0, INF),
|
||||||
float.fromhex('0x1.0000000000001p+0'))
|
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
|
# x == y: y is returned
|
||||||
self.assertEqual(math.nextafter(2.0, 2.0), 2.0)
|
for steps in range(1, 5):
|
||||||
self.assertEqualSign(math.nextafter(-0.0, +0.0), +0.0)
|
self.assertEqual(math.nextafter(2.0, 2.0, steps=steps), 2.0)
|
||||||
self.assertEqualSign(math.nextafter(+0.0, -0.0), -0.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
|
# around 0.0
|
||||||
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
|
smallest_subnormal = sys.float_info.min * sys.float_info.epsilon
|
||||||
@ -2325,6 +2334,11 @@ class MathTests(unittest.TestCase):
|
|||||||
self.assertIsNaN(math.nextafter(1.0, NAN))
|
self.assertIsNaN(math.nextafter(1.0, NAN))
|
||||||
self.assertIsNaN(math.nextafter(NAN, 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
|
@requires_IEEE_754
|
||||||
def test_ulp(self):
|
def test_ulp(self):
|
||||||
self.assertEqual(math.ulp(1.0), sys.float_info.epsilon)
|
self.assertEqual(math.ulp(1.0), sys.float_info.epsilon)
|
||||||
|
41
Lib/test/test_math_property.py
Normal file
41
Lib/test/test_math_property.py
Normal file
@ -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)
|
@ -0,0 +1 @@
|
|||||||
|
Support multiple steps in :func:`math.nextafter`. Patch by Shantanu Jain and Matthias Gorgens.
|
55
Modules/clinic/mathmodule.c.h
generated
55
Modules/clinic/mathmodule.c.h
generated
@ -826,25 +826,59 @@ exit:
|
|||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(math_nextafter__doc__,
|
PyDoc_STRVAR(math_nextafter__doc__,
|
||||||
"nextafter($module, x, y, /)\n"
|
"nextafter($module, x, y, /, *, steps=None)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Return the next floating-point value after x towards y.");
|
"Return the floating-point value the given number of steps after x towards y.\n"
|
||||||
|
"\n"
|
||||||
|
"If steps is not specified or is None, it defaults to 1.\n"
|
||||||
|
"\n"
|
||||||
|
"Raises a TypeError, if x or y is not a double, or if steps is not an integer.\n"
|
||||||
|
"Raises ValueError if steps is negative.");
|
||||||
|
|
||||||
#define MATH_NEXTAFTER_METHODDEF \
|
#define MATH_NEXTAFTER_METHODDEF \
|
||||||
{"nextafter", _PyCFunction_CAST(math_nextafter), METH_FASTCALL, math_nextafter__doc__},
|
{"nextafter", _PyCFunction_CAST(math_nextafter), METH_FASTCALL|METH_KEYWORDS, math_nextafter__doc__},
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
math_nextafter_impl(PyObject *module, double x, double y);
|
math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps);
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
math_nextafter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
|
math_nextafter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
|
||||||
{
|
{
|
||||||
PyObject *return_value = NULL;
|
PyObject *return_value = NULL;
|
||||||
|
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
|
||||||
|
|
||||||
|
#define NUM_KEYWORDS 1
|
||||||
|
static struct {
|
||||||
|
PyGC_Head _this_is_not_used;
|
||||||
|
PyObject_VAR_HEAD
|
||||||
|
PyObject *ob_item[NUM_KEYWORDS];
|
||||||
|
} _kwtuple = {
|
||||||
|
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
|
||||||
|
.ob_item = { &_Py_ID(steps), },
|
||||||
|
};
|
||||||
|
#undef NUM_KEYWORDS
|
||||||
|
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
|
||||||
|
|
||||||
|
#else // !Py_BUILD_CORE
|
||||||
|
# define KWTUPLE NULL
|
||||||
|
#endif // !Py_BUILD_CORE
|
||||||
|
|
||||||
|
static const char * const _keywords[] = {"", "", "steps", NULL};
|
||||||
|
static _PyArg_Parser _parser = {
|
||||||
|
.keywords = _keywords,
|
||||||
|
.fname = "nextafter",
|
||||||
|
.kwtuple = KWTUPLE,
|
||||||
|
};
|
||||||
|
#undef KWTUPLE
|
||||||
|
PyObject *argsbuf[3];
|
||||||
|
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2;
|
||||||
double x;
|
double x;
|
||||||
double y;
|
double y;
|
||||||
|
PyObject *steps = Py_None;
|
||||||
|
|
||||||
if (!_PyArg_CheckPositional("nextafter", nargs, 2, 2)) {
|
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
|
||||||
|
if (!args) {
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (PyFloat_CheckExact(args[0])) {
|
if (PyFloat_CheckExact(args[0])) {
|
||||||
@ -867,7 +901,12 @@ math_nextafter(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
|
|||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return_value = math_nextafter_impl(module, x, y);
|
if (!noptargs) {
|
||||||
|
goto skip_optional_kwonly;
|
||||||
|
}
|
||||||
|
steps = args[2];
|
||||||
|
skip_optional_kwonly:
|
||||||
|
return_value = math_nextafter_impl(module, x, y, steps);
|
||||||
|
|
||||||
exit:
|
exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
@ -911,4 +950,4 @@ math_ulp(PyObject *module, PyObject *arg)
|
|||||||
exit:
|
exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
/*[clinic end generated code: output=a6437a3ba18c486a input=a9049054013a1b77]*/
|
/*[clinic end generated code: output=91a0357265a2a553 input=a9049054013a1b77]*/
|
||||||
|
@ -3864,13 +3864,20 @@ math.nextafter
|
|||||||
x: double
|
x: double
|
||||||
y: double
|
y: double
|
||||||
/
|
/
|
||||||
|
*
|
||||||
|
steps: object = None
|
||||||
|
|
||||||
Return the next floating-point value after x towards y.
|
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.
|
||||||
|
|
||||||
|
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]*/
|
[clinic start generated code]*/
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
math_nextafter_impl(PyObject *module, double x, double y)
|
math_nextafter_impl(PyObject *module, double x, double y, PyObject *steps)
|
||||||
/*[clinic end generated code: output=750c8266c1c540ce input=02b2d50cd1d9f9b6]*/
|
/*[clinic end generated code: output=cc6511f02afc099e input=7f2a5842112af2b4]*/
|
||||||
{
|
{
|
||||||
#if defined(_AIX)
|
#if defined(_AIX)
|
||||||
if (x == y) {
|
if (x == y) {
|
||||||
@ -3885,8 +3892,102 @@ math_nextafter_impl(PyObject *module, double x, double y)
|
|||||||
return PyFloat_FromDouble(y);
|
return PyFloat_FromDouble(y);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if (steps == Py_None) {
|
||||||
|
// fast path: we default to one step.
|
||||||
return PyFloat_FromDouble(nextafter(x, y));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user