Issue #19543: Implementation of isclose as per PEP 485

For details, see:
PEP 0485 -- A Function for testing approximate equality

Functions added: math.isclose() and cmath.isclose().

Original code by Chris Barker. Patch by Tal Einat.
This commit is contained in:
Tal Einat 2015-05-31 22:05:00 +03:00
parent 439c5fe3ae
commit d5519ed7f4
9 changed files with 450 additions and 1 deletions

View File

@ -207,6 +207,38 @@ Classification functions
and ``False`` otherwise. and ``False`` otherwise.
.. function:: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
Return ``True`` if the values *a* and *b* are close to each other and
``False`` otherwise.
Whether or not two values are considered close is determined according to
given absolute and relative tolerances.
*rel_tol* is the relative tolerance -- it is the maximum allowed difference
between *a* and *b*, relative to the larger absolute value of *a* or *b*.
For example, to set a tolerance of 5%, pass ``rel_tol=0.05``. The default
tolerance is ``1e-09``, which assures that the two values are the same
within about 9 decimal digits. *rel_tol* must be greater than zero.
*abs_tol* is the minimum absolute tolerance -- useful for comparisons near
zero. *abs_tol* must be at least zero.
If no errors occur, the result will be:
``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be
handled according to IEEE rules. Specifically, ``NaN`` is not considered
close to any other value, including ``NaN``. ``inf`` and ``-inf`` are only
considered close to themselves.
.. versionadded:: 3.5
.. seealso::
:pep:`485` -- A function for testing approximate equality
Constants Constants
--------- ---------

View File

@ -110,6 +110,38 @@ Number-theoretic and representation functions
.. versionadded:: 3.5 .. versionadded:: 3.5
.. function:: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
Return ``True`` if the values *a* and *b* are close to each other and
``False`` otherwise.
Whether or not two values are considered close is determined according to
given absolute and relative tolerances.
*rel_tol* is the relative tolerance -- it is the maximum allowed difference
between *a* and *b*, relative to the larger absolute value of *a* or *b*.
For example, to set a tolerance of 5%, pass ``rel_tol=0.05``. The default
tolerance is ``1e-09``, which assures that the two values are the same
within about 9 decimal digits. *rel_tol* must be greater than zero.
*abs_tol* is the minimum absolute tolerance -- useful for comparisons near
zero. *abs_tol* must be at least zero.
If no errors occur, the result will be:
``abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)``.
The IEEE 754 special values of ``NaN``, ``inf``, and ``-inf`` will be
handled according to IEEE rules. Specifically, ``NaN`` is not considered
close to any other value, including ``NaN``. ``inf`` and ``-inf`` are only
considered close to themselves.
.. versionadded:: 3.5
.. seealso::
:pep:`485` -- A function for testing approximate equality
.. function:: isfinite(x) .. function:: isfinite(x)
Return ``True`` if *x* is neither an infinity nor a NaN, and Return ``True`` if *x* is neither an infinity nor a NaN, and

View File

@ -285,6 +285,18 @@ rather than being restricted to ASCII.
:pep:`488` -- Multi-phase extension module initialization :pep:`488` -- Multi-phase extension module initialization
PEP 485: A function for testing approximate equality
----------------------------------------------------
:pep:`485` adds the :func:`math.isclose` and :func:`cmath.isclose`
functions which tell whether two values are approximately equal or
"close" to each other. Whether or not two values are considered
close is determined according to given absolute and relative tolerances.
.. seealso::
:pep:`485` -- A function for testing approximate equality
Other Language Changes Other Language Changes
====================== ======================
@ -346,6 +358,13 @@ cgi
* :class:`~cgi.FieldStorage` now supports the context management protocol. * :class:`~cgi.FieldStorage` now supports the context management protocol.
(Contributed by Berker Peksag in :issue:`20289`.) (Contributed by Berker Peksag in :issue:`20289`.)
cmath
-----
* :func:`cmath.isclose` function added.
(Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
code code
---- ----
@ -578,6 +597,8 @@ math
* :data:`math.inf` and :data:`math.nan` constants added. (Contributed by Mark * :data:`math.inf` and :data:`math.nan` constants added. (Contributed by Mark
Dickinson in :issue:`23185`.) Dickinson in :issue:`23185`.)
* :func:`math.isclose` function added.
(Contributed by Chris Barker and Tal Einat in :issue:`24270`.)
shutil shutil
------ ------

View File

@ -1,5 +1,6 @@
from test.support import requires_IEEE_754 from test.support import requires_IEEE_754
from test.test_math import parse_testfile, test_file from test.test_math import parse_testfile, test_file
import test.test_math as test_math
import unittest import unittest
import cmath, math import cmath, math
from cmath import phase, polar, rect, pi from cmath import phase, polar, rect, pi
@ -529,5 +530,46 @@ class CMathTests(unittest.TestCase):
self.assertComplexIdentical(cmath.atanh(z), z) self.assertComplexIdentical(cmath.atanh(z), z)
class IsCloseTests(test_math.IsCloseTests):
isclose = cmath.isclose
def test_reject_complex_tolerances(self):
with self.assertRaises(TypeError):
self.isclose(1j, 1j, rel_tol=1j)
with self.assertRaises(TypeError):
self.isclose(1j, 1j, abs_tol=1j)
with self.assertRaises(TypeError):
self.isclose(1j, 1j, rel_tol=1j, abs_tol=1j)
def test_complex_values(self):
# test complex values that are close to within 12 decimal places
complex_examples = [(1.0+1.0j, 1.000000000001+1.0j),
(1.0+1.0j, 1.0+1.000000000001j),
(-1.0+1.0j, -1.000000000001+1.0j),
(1.0-1.0j, 1.0-0.999999999999j),
]
self.assertAllClose(complex_examples, rel_tol=1e-12)
self.assertAllNotClose(complex_examples, rel_tol=1e-13)
def test_complex_near_zero(self):
# test values near zero that are near to within three decimal places
near_zero_examples = [(0.001j, 0),
(0.001, 0),
(0.001+0.001j, 0),
(-0.001+0.001j, 0),
(0.001-0.001j, 0),
(-0.001-0.001j, 0),
]
self.assertAllClose(near_zero_examples, abs_tol=1.5e-03)
self.assertAllNotClose(near_zero_examples, abs_tol=0.5e-03)
self.assertIsClose(0.001-0.001j, 0.001+0.001j, abs_tol=2e-03)
self.assertIsNotClose(0.001-0.001j, 0.001+0.001j, abs_tol=1e-03)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -1166,10 +1166,131 @@ class MathTests(unittest.TestCase):
'\n '.join(failures)) '\n '.join(failures))
class IsCloseTests(unittest.TestCase):
isclose = math.isclose # sublcasses should override this
def assertIsClose(self, a, b, *args, **kwargs):
self.assertTrue(self.isclose(a, b, *args, **kwargs),
msg="%s and %s should be close!" % (a, b))
def assertIsNotClose(self, a, b, *args, **kwargs):
self.assertFalse(self.isclose(a, b, *args, **kwargs),
msg="%s and %s should not be close!" % (a, b))
def assertAllClose(self, examples, *args, **kwargs):
for a, b in examples:
self.assertIsClose(a, b, *args, **kwargs)
def assertAllNotClose(self, examples, *args, **kwargs):
for a, b in examples:
self.assertIsNotClose(a, b, *args, **kwargs)
def test_negative_tolerances(self):
# ValueError should be raised if either tolerance is less than zero
with self.assertRaises(ValueError):
self.assertIsClose(1, 1, rel_tol=-1e-100)
with self.assertRaises(ValueError):
self.assertIsClose(1, 1, rel_tol=1e-100, abs_tol=-1e10)
def test_identical(self):
# identical values must test as close
identical_examples = [(2.0, 2.0),
(0.1e200, 0.1e200),
(1.123e-300, 1.123e-300),
(12345, 12345.0),
(0.0, -0.0),
(345678, 345678)]
self.assertAllClose(identical_examples, rel_tol=0.0, abs_tol=0.0)
def test_eight_decimal_places(self):
# examples that are close to 1e-8, but not 1e-9
eight_decimal_places_examples = [(1e8, 1e8 + 1),
(-1e-8, -1.000000009e-8),
(1.12345678, 1.12345679)]
self.assertAllClose(eight_decimal_places_examples, rel_tol=1e-8)
self.assertAllNotClose(eight_decimal_places_examples, rel_tol=1e-9)
def test_near_zero(self):
# values close to zero
near_zero_examples = [(1e-9, 0.0),
(-1e-9, 0.0),
(-1e-150, 0.0)]
# these should not be close to any rel_tol
self.assertAllNotClose(near_zero_examples, rel_tol=0.9)
# these should be close to abs_tol=1e-8
self.assertAllClose(near_zero_examples, abs_tol=1e-8)
def test_identical_infinite(self):
# these are close regardless of tolerance -- i.e. they are equal
self.assertIsClose(INF, INF)
self.assertIsClose(INF, INF, abs_tol=0.0)
self.assertIsClose(NINF, NINF)
self.assertIsClose(NINF, NINF, abs_tol=0.0)
def test_inf_ninf_nan(self):
# these should never be close (following IEEE 754 rules for equality)
not_close_examples = [(NAN, NAN),
(NAN, 1e-100),
(1e-100, NAN),
(INF, NAN),
(NAN, INF),
(INF, NINF),
(INF, 1.0),
(1.0, INF),
(INF, 1e308),
(1e308, INF)]
# use largest reasonable tolerance
self.assertAllNotClose(not_close_examples, abs_tol=0.999999999999999)
def test_zero_tolerance(self):
# test with zero tolerance
zero_tolerance_close_examples = [(1.0, 1.0),
(-3.4, -3.4),
(-1e-300, -1e-300)]
self.assertAllClose(zero_tolerance_close_examples, rel_tol=0.0)
zero_tolerance_not_close_examples = [(1.0, 1.000000000000001),
(0.99999999999999, 1.0),
(1.0e200, .999999999999999e200)]
self.assertAllNotClose(zero_tolerance_not_close_examples, rel_tol=0.0)
def test_assymetry(self):
# test the assymetry example from PEP 485
self.assertAllClose([(9, 10), (10, 9)], rel_tol=0.1)
def test_integers(self):
# test with integer values
integer_examples = [(100000001, 100000000),
(123456789, 123456788)]
self.assertAllClose(integer_examples, rel_tol=1e-8)
self.assertAllNotClose(integer_examples, rel_tol=1e-9)
def test_decimals(self):
# test with Decimal values
from decimal import Decimal
decimal_examples = [(Decimal('1.00000001'), Decimal('1.0')),
(Decimal('1.00000001e-20'), Decimal('1.0e-20')),
(Decimal('1.00000001e-100'), Decimal('1.0e-100'))]
self.assertAllClose(decimal_examples, rel_tol=1e-8)
self.assertAllNotClose(decimal_examples, rel_tol=1e-9)
def test_fractions(self):
# test with Fraction values
from fractions import Fraction
# could use some more examples here!
fraction_examples = [(Fraction(1, 100000000) + 1, Fraction(1))]
self.assertAllClose(fraction_examples, rel_tol=1e-8)
self.assertAllNotClose(fraction_examples, rel_tol=1e-9)
def test_main(): def test_main():
from doctest import DocFileSuite from doctest import DocFileSuite
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MathTests)) suite.addTest(unittest.makeSuite(MathTests))
suite.addTest(unittest.makeSuite(IsCloseTests))
suite.addTest(DocFileSuite("ieee754.txt")) suite.addTest(DocFileSuite("ieee754.txt"))
run_unittest(suite) run_unittest(suite)

View File

@ -273,6 +273,9 @@ Library
- Issue #24298: Fix inspect.signature() to correctly unwrap wrappers - Issue #24298: Fix inspect.signature() to correctly unwrap wrappers
around bound methods. around bound methods.
- Issue #24270: Add math.isclose() and cmath.isclose() functions as per PEP 485.
Contributed by Chris Barker and Tal Einat.
IDLE IDLE
---- ----

View File

@ -806,4 +806,55 @@ cmath_isinf(PyModuleDef *module, PyObject *arg)
exit: exit:
return return_value; return return_value;
} }
/*[clinic end generated code: output=274f59792cf4f418 input=a9049054013a1b77]*/
PyDoc_STRVAR(cmath_isclose__doc__,
"isclose($module, /, a, b, *, rel_tol=1e-09, abs_tol=0.0)\n"
"--\n"
"\n"
"Determine whether two complex numbers are close in value.\n"
"\n"
" rel_tol\n"
" maximum difference for being considered \"close\", relative to the\n"
" magnitude of the input values\n"
" abs_tol\n"
" maximum difference for being considered \"close\", regardless of the\n"
" magnitude of the input values\n"
"\n"
"Return True if a is close in value to b, and False otherwise.\n"
"\n"
"For the values to be considered close, the difference between them must be\n"
"smaller than at least one of the tolerances.\n"
"\n"
"-inf, inf and NaN behave similarly to the IEEE 754 Standard. That is, NaN is\n"
"not close to anything, even itself. inf and -inf are only close to themselves.");
#define CMATH_ISCLOSE_METHODDEF \
{"isclose", (PyCFunction)cmath_isclose, METH_VARARGS|METH_KEYWORDS, cmath_isclose__doc__},
static int
cmath_isclose_impl(PyModuleDef *module, Py_complex a, Py_complex b,
double rel_tol, double abs_tol);
static PyObject *
cmath_isclose(PyModuleDef *module, PyObject *args, PyObject *kwargs)
{
PyObject *return_value = NULL;
static char *_keywords[] = {"a", "b", "rel_tol", "abs_tol", NULL};
Py_complex a;
Py_complex b;
double rel_tol = 1e-09;
double abs_tol = 0.0;
int _return_value;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "DD|$dd:isclose", _keywords,
&a, &b, &rel_tol, &abs_tol))
goto exit;
_return_value = cmath_isclose_impl(module, a, b, rel_tol, abs_tol);
if ((_return_value == -1) && PyErr_Occurred())
goto exit;
return_value = PyBool_FromLong((long)_return_value);
exit:
return return_value;
}
/*[clinic end generated code: output=229e9c48c9d27362 input=a9049054013a1b77]*/

View File

@ -1114,6 +1114,73 @@ cmath_isinf_impl(PyModuleDef *module, Py_complex z)
Py_IS_INFINITY(z.imag)); Py_IS_INFINITY(z.imag));
} }
/*[clinic input]
cmath.isclose -> bool
a: Py_complex
b: Py_complex
*
rel_tol: double = 1e-09
maximum difference for being considered "close", relative to the
magnitude of the input values
abs_tol: double = 0.0
maximum difference for being considered "close", regardless of the
magnitude of the input values
Determine whether two complex numbers are close in value.
Return True if a is close in value to b, and False otherwise.
For the values to be considered close, the difference between them must be
smaller than at least one of the tolerances.
-inf, inf and NaN behave similarly to the IEEE 754 Standard. That is, NaN is
not close to anything, even itself. inf and -inf are only close to themselves.
[clinic start generated code]*/
static int
cmath_isclose_impl(PyModuleDef *module, Py_complex a, Py_complex b,
double rel_tol, double abs_tol)
/*[clinic end generated code: output=da0c535fb54e2310 input=df9636d7de1d4ac3]*/
{
double diff;
/* sanity check on the inputs */
if (rel_tol < 0.0 || abs_tol < 0.0 ) {
PyErr_SetString(PyExc_ValueError,
"tolerances must be non-negative");
return -1;
}
if ( (a.real == b.real) && (a.imag == b.imag) ) {
/* short circuit exact equality -- needed to catch two infinities of
the same sign. And perhaps speeds things up a bit sometimes.
*/
return 1;
}
/* This catches the case of two infinities of opposite sign, or
one infinity and one finite number. Two infinities of opposite
sign would otherwise have an infinite relative tolerance.
Two infinities of the same sign are caught by the equality check
above.
*/
if (Py_IS_INFINITY(a.real) || Py_IS_INFINITY(a.imag) ||
Py_IS_INFINITY(b.real) || Py_IS_INFINITY(b.imag)) {
return 0;
}
/* now do the regular computation
this is essentially the "weak" test from the Boost library
*/
diff = _Py_c_abs(_Py_c_diff(a, b));
return (((diff <= rel_tol * _Py_c_abs(b)) ||
(diff <= rel_tol * _Py_c_abs(a))) ||
(diff <= abs_tol));
}
PyDoc_STRVAR(module_doc, PyDoc_STRVAR(module_doc,
"This module is always available. It provides access to mathematical\n" "This module is always available. It provides access to mathematical\n"
@ -1129,6 +1196,7 @@ static PyMethodDef cmath_methods[] = {
CMATH_COS_METHODDEF CMATH_COS_METHODDEF
CMATH_COSH_METHODDEF CMATH_COSH_METHODDEF
CMATH_EXP_METHODDEF CMATH_EXP_METHODDEF
CMATH_ISCLOSE_METHODDEF
CMATH_ISFINITE_METHODDEF CMATH_ISFINITE_METHODDEF
CMATH_ISINF_METHODDEF CMATH_ISINF_METHODDEF
CMATH_ISNAN_METHODDEF CMATH_ISNAN_METHODDEF

View File

@ -1990,6 +1990,83 @@ PyDoc_STRVAR(math_isinf_doc,
"isinf(x) -> bool\n\n\ "isinf(x) -> bool\n\n\
Return True if x is a positive or negative infinity, and False otherwise."); Return True if x is a positive or negative infinity, and False otherwise.");
static PyObject *
math_isclose(PyObject *self, PyObject *args, PyObject *kwargs)
{
double a, b;
double rel_tol = 1e-9;
double abs_tol = 0.0;
double diff = 0.0;
long result = 0;
static char *keywords[] = {"a", "b", "rel_tol", "abs_tol", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "dd|$dd:isclose",
keywords,
&a, &b, &rel_tol, &abs_tol
))
return NULL;
/* sanity check on the inputs */
if (rel_tol < 0.0 || abs_tol < 0.0 ) {
PyErr_SetString(PyExc_ValueError,
"tolerances must be non-negative");
return NULL;
}
if ( a == b ) {
/* short circuit exact equality -- needed to catch two infinities of
the same sign. And perhaps speeds things up a bit sometimes.
*/
Py_RETURN_TRUE;
}
/* This catches the case of two infinities of opposite sign, or
one infinity and one finite number. Two infinities of opposite
sign would otherwise have an infinite relative tolerance.
Two infinities of the same sign are caught by the equality check
above.
*/
if (Py_IS_INFINITY(a) || Py_IS_INFINITY(b)) {
Py_RETURN_FALSE;
}
/* now do the regular computation
this is essentially the "weak" test from the Boost library
*/
diff = fabs(b - a);
result = (((diff <= fabs(rel_tol * b)) ||
(diff <= fabs(rel_tol * a))) ||
(diff <= abs_tol));
return PyBool_FromLong(result);
}
PyDoc_STRVAR(math_isclose_doc,
"is_close(a, b, *, rel_tol=1e-09, abs_tol=0.0) -> bool\n"
"\n"
"Determine whether two floating point numbers are close in value.\n"
"\n"
" rel_tol\n"
" maximum difference for being considered \"close\", relative to the\n"
" magnitude of the input values\n"
" abs_tol\n"
" maximum difference for being considered \"close\", regardless of the\n"
" magnitude of the input values\n"
"\n"
"Return True if a is close in value to b, and False otherwise.\n"
"\n"
"For the values to be considered close, the difference between them\n"
"must be smaller than at least one of the tolerances.\n"
"\n"
"-inf, inf and NaN behave similarly to the IEEE 754 Standard. That\n"
"is, NaN is not close to anything, even itself. inf and -inf are\n"
"only close to themselves.");
static PyMethodDef math_methods[] = { static PyMethodDef math_methods[] = {
{"acos", math_acos, METH_O, math_acos_doc}, {"acos", math_acos, METH_O, math_acos_doc},
{"acosh", math_acosh, METH_O, math_acosh_doc}, {"acosh", math_acosh, METH_O, math_acosh_doc},
@ -2016,6 +2093,8 @@ static PyMethodDef math_methods[] = {
{"gamma", math_gamma, METH_O, math_gamma_doc}, {"gamma", math_gamma, METH_O, math_gamma_doc},
{"gcd", math_gcd, METH_VARARGS, math_gcd_doc}, {"gcd", math_gcd, METH_VARARGS, math_gcd_doc},
{"hypot", math_hypot, METH_VARARGS, math_hypot_doc}, {"hypot", math_hypot, METH_VARARGS, math_hypot_doc},
{"isclose", (PyCFunction) math_isclose, METH_VARARGS | METH_KEYWORDS,
math_isclose_doc},
{"isfinite", math_isfinite, METH_O, math_isfinite_doc}, {"isfinite", math_isfinite, METH_O, math_isfinite_doc},
{"isinf", math_isinf, METH_O, math_isinf_doc}, {"isinf", math_isinf, METH_O, math_isinf_doc},
{"isnan", math_isnan, METH_O, math_isnan_doc}, {"isnan", math_isnan, METH_O, math_isnan_doc},