From ec1528372c4469dde9b5aeadda68057077e06e16 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 7 Jul 2018 00:52:49 -0700 Subject: [PATCH 01/21] Segfaulting rough draft without NaN support or raising Overflow --- Modules/mathmodule.c | 68 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 5d9fe5aa71a679..e1720df214c684 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -56,6 +56,73 @@ raised for division by zero and mod by zero. #include "_math.h" #include "clinic/mathmodule.c.h" +#include + +/* Notes: + + if (!Py_IS_FINITE(x)) { + if (Py_IS_NAN(x) || x > 0.0) + + ? FPE protection + + ? Don't scale if max == 0.0 + + ? If only one value, it is the hypot. + + Any infinity gives infinity + If no infinity, any NaN gives a NaN + + hypot(x=1) -> TypeError // no keyword args + hypot(-10.5) -> 10.5 // flip the sign to positive + hypot(-Inf) -> Inf; // flip the sign to positive + hypot() -> 0.0 // degrade like sum([]) +*/ + + +/* AC: cannot convert yet, waiting for *args support */ +static PyObject * +m_hypot(PyObject *self, PyObject *args) +{ + Py_ssize_t i, n; + double max = 0.0; + double x; + double csum = 0.0; + double **farray; + PyObject *item; + + n = PyTuple_GET_SIZE(args); + farray = PyMem_Malloc(n * sizeof(double)); + if (farray == NULL) { + return NULL; + } + for (i=0 ; i\n", x); + x = fabs(x); + fprintf(stderr, "|%lf|\n", x); + (*farray)[i] = x; + if (x > max) { + max = x; + } + } + if (n <= 1 || max == 0.0) { + PyMem_Free(farray); + return PyFloat_FromDouble(max); + } + for (i=0 ; i Date: Sat, 7 Jul 2018 02:01:58 -0700 Subject: [PATCH 02/21] Working verion. Double pass over input tuple. No inf/nan handling. --- Modules/mathmodule.c | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index e1720df214c684..20ce4cc6496561 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -56,7 +56,6 @@ raised for division by zero and mod by zero. #include "_math.h" #include "clinic/mathmodule.c.h" -#include /* Notes: @@ -78,50 +77,41 @@ raised for division by zero and mod by zero. hypot() -> 0.0 // degrade like sum([]) */ - /* AC: cannot convert yet, waiting for *args support */ static PyObject * m_hypot(PyObject *self, PyObject *args) { Py_ssize_t i, n; + PyObject *item; double max = 0.0; - double x; double csum = 0.0; - double **farray; - PyObject *item; + double x; n = PyTuple_GET_SIZE(args); - farray = PyMem_Malloc(n * sizeof(double)); - if (farray == NULL) { - return NULL; - } for (i=0 ; i\n", x); x = fabs(x); - fprintf(stderr, "|%lf|\n", x); - (*farray)[i] = x; if (x > max) { max = x; } } if (n <= 1 || max == 0.0) { - PyMem_Free(farray); return PyFloat_FromDouble(max); } for (i=0 ; i Date: Sat, 7 Jul 2018 02:08:42 -0700 Subject: [PATCH 03/21] Simplify. Remove unnecessary second error check. Remove n<=1 special case. --- Modules/mathmodule.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 20ce4cc6496561..3ca2cc698cba27 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -99,16 +99,12 @@ m_hypot(PyObject *self, PyObject *args) max = x; } } - if (n <= 1 || max == 0.0) { - return PyFloat_FromDouble(max); + if (max == 0.0) { + return PyFloat_FromDouble(0.0); } for (i=0 ; i Date: Tue, 24 Jul 2018 08:06:06 -0700 Subject: [PATCH 04/21] Add some tests --- Lib/test/test_math.py | 23 +++++++++++++++++++++++ Modules/mathmodule.c | 9 ++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 44785d3e49a255..b5ca98aac877bb 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -731,6 +731,29 @@ def testHypot(self): self.assertTrue(math.isnan(math.hypot(1.0, NAN))) self.assertTrue(math.isnan(math.hypot(NAN, -2.0))) + def test_multi_hypot(self): + from decimal import Decimal as D + + hypot = math.mh + + self.assertEqual(hypot(12.0, 5.0), 13.0) # Float inputs. Exact output. + self.assertEqual(hypot(12, 5), 13) # Int inputs + self.assertEqual(hypot(D(12), D(5)), 13) # Decimal inputs + + self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero + self.assertEqual(hypot(-10.5), 10.5) # Negative input + + with self.assertRaises(TypeError): + hypot(x=1) # Reject keyword args + + # Test 0 to 4 dimensional inputs + args = math.e, math.pi, math.sqrt(2.0), math.gamma(3.5) + for i in range(len(args)+1): + self.assertAlmostEqual( + hypot(*args[:i]), + math.sqrt(sum(s**2 for s in args[:i])) + ) + def testLdexp(self): self.assertRaises(TypeError, math.ldexp) self.ftest('ldexp(0,1)', math.ldexp(0,1), 0) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 3ca2cc698cba27..5a266423bbd21b 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -71,10 +71,13 @@ raised for division by zero and mod by zero. Any infinity gives infinity If no infinity, any NaN gives a NaN - hypot(x=1) -> TypeError // no keyword args - hypot(-10.5) -> 10.5 // flip the sign to positive + * hypot(x=1) -> TypeError // no keyword args + * hypot(-10.5) -> 10.5 // flip the sign to positive hypot(-Inf) -> Inf; // flip the sign to positive - hypot() -> 0.0 // degrade like sum([]) + * hypot() -> 0.0 // degrade like sum([]) or reduce(hypot, sides, 0.0) + * hypot(0) -> 0.0 // don't divide by zero + * hypot(3, 4) // ints converted to floats + * hypot(D('3.0'), D('4.0')) */ /* AC: cannot convert yet, waiting for *args support */ From de3c16bb7dc87353620703402aac017a5a9492e7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 07:58:27 -0700 Subject: [PATCH 05/21] Add support for special values. --- Lib/test/test_math.py | 21 +++++++++++++++++++++ Modules/mathmodule.c | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index b5ca98aac877bb..cfd4859cc18f8f 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -754,6 +754,27 @@ def test_multi_hypot(self): math.sqrt(sum(s**2 for s in args[:i])) ) + # Test special values. + # Any infinity gives positive infinity. + self.assertEqual(hypot(INF), INF) + self.assertEqual(hypot(0, INF), INF) + self.assertEqual(hypot(10, INF), INF) + self.assertEqual(hypot(-10, INF), INF) + self.assertEqual(hypot(NAN, INF), INF) + self.assertEqual(hypot(-INF, INF), INF) + self.assertEqual(hypot(-INF, -INF), INF) + self.assertEqual(hypot(10, -INF), INF) + # If no infinity, any NaN gives a Nan. + self.assertTrue(math.isnan(hypot(NAN))) + self.assertTrue(math.isnan(hypot(0, NAN))) + self.assertTrue(math.isnan(hypot(NAN, 10))) + self.assertTrue(math.isnan(hypot(NAN, NAN))) + self.assertTrue(math.isnan(hypot(NAN))) + + # Exact tests + # hypot(*[value*n]) == value * math.sqrt(n) + + def testLdexp(self): self.assertRaises(TypeError, math.ldexp) self.ftest('ldexp(0,1)', math.ldexp(0,1), 0) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 5a266423bbd21b..68351f3d4c91fc 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -89,6 +89,8 @@ m_hypot(PyObject *self, PyObject *args) double max = 0.0; double csum = 0.0; double x; + int found_inf = 0; + int found_nan = 0; n = PyTuple_GET_SIZE(args); for (i=0 ; i max) { max = x; } } + if (found_inf) { + return PyFloat_FromDouble(max); + } + if (found_nan) { + return PyFloat_FromDouble(Py_NAN); + } if (max == 0.0) { return PyFloat_FromDouble(0.0); } From 18babd43373adbd695ff190067e75cc78e994605 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 09:01:09 -0700 Subject: [PATCH 06/21] Test scaling for large values --- Lib/test/test_math.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index cfd4859cc18f8f..dc9509e85337ba 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -771,9 +771,10 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN, NAN))) self.assertTrue(math.isnan(hypot(NAN))) - # Exact tests - # hypot(*[value*n]) == value * math.sqrt(n) - + # Verify scaling for large values + halfmax = sys.float_info.max / 2.0 + for n in range(10): + self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) def testLdexp(self): self.assertRaises(TypeError, math.ldexp) From 6c412d2438274a7b5bd8cd0d5462d0e36123b9ef Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 17:54:17 -0700 Subject: [PATCH 07/21] Add tests for small values --- Lib/test/test_math.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index dc9509e85337ba..3cbd6ab515ffcc 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -771,10 +771,16 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN, NAN))) self.assertTrue(math.isnan(hypot(NAN))) - # Verify scaling for large values + # Verify scaling for extremely large values halfmax = sys.float_info.max / 2.0 for n in range(10): self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) + # Verify scaling for extremely small values + small = sys.float_info.min + a, b, c = 5, 12, 13 + for exp in range(32): + scale = small / 2 ** exp + self.assertEqual(math.hypot(a*scale, b*scale), c*scale) def testLdexp(self): self.assertRaises(TypeError, math.ldexp) From 780024b7314752b54c8532b7425a0b05563102c8 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 18:12:26 -0700 Subject: [PATCH 08/21] Neaten-up test cases --- Lib/test/test_math.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 3cbd6ab515ffcc..a78e398c5ae1aa 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -732,29 +732,36 @@ def testHypot(self): self.assertTrue(math.isnan(math.hypot(NAN, -2.0))) def test_multi_hypot(self): - from decimal import Decimal as D + from decimal import Decimal + from fractions import Fraction hypot = math.mh - self.assertEqual(hypot(12.0, 5.0), 13.0) # Float inputs. Exact output. - self.assertEqual(hypot(12, 5), 13) # Int inputs - self.assertEqual(hypot(D(12), D(5)), 13) # Decimal inputs - - self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero - self.assertEqual(hypot(-10.5), 10.5) # Negative input - - with self.assertRaises(TypeError): - hypot(x=1) # Reject keyword args - - # Test 0 to 4 dimensional inputs - args = math.e, math.pi, math.sqrt(2.0), math.gamma(3.5) + # Test different numbers of arguments from zero to five + args = math.e, math.pi, math.sqrt(2.0), math.gamma(3.5), math.sin(2.1) for i in range(len(args)+1): self.assertAlmostEqual( hypot(*args[:i]), math.sqrt(sum(s**2 for s in args[:i])) ) - # Test special values. + # Test allowable types (those with __float__) + self.assertEqual(hypot(12.0, 5.0), 13.0) + self.assertEqual(hypot(12, 5), 13) + self.assertEqual(hypot(Decimal(12), Decimal(5)), 13) + self.assertEqual(hypot(Fraction(12, 32), Fraction(5, 32)), Fraction(13, 32)) + self.assertEqual(hypot(bool(1), bool(0), bool(1), bool(1)), math.sqrt(3)) + + # Test corner cases + self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero + self.assertEqual(hypot(-10.5), 10.5) # Negative input + + # Test handling of bad arguments + with self.assertRaises(TypeError): # Reject keyword args + hypot(x=1) + with self.assertRaises(TypeError): # Reject values without __float__ + hypot(1.1, 'string', 2.2) + # Any infinity gives positive infinity. self.assertEqual(hypot(INF), INF) self.assertEqual(hypot(0, INF), INF) @@ -764,6 +771,7 @@ def test_multi_hypot(self): self.assertEqual(hypot(-INF, INF), INF) self.assertEqual(hypot(-INF, -INF), INF) self.assertEqual(hypot(10, -INF), INF) + # If no infinity, any NaN gives a Nan. self.assertTrue(math.isnan(hypot(NAN))) self.assertTrue(math.isnan(hypot(0, NAN))) @@ -775,12 +783,12 @@ def test_multi_hypot(self): halfmax = sys.float_info.max / 2.0 for n in range(10): self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) + # Verify scaling for extremely small values small = sys.float_info.min - a, b, c = 5, 12, 13 for exp in range(32): scale = small / 2 ** exp - self.assertEqual(math.hypot(a*scale, b*scale), c*scale) + self.assertEqual(math.hypot(12*scale, 5*scale), 13*scale) def testLdexp(self): self.assertRaises(TypeError, math.ldexp) From d82df990c62aeb6bfa2c59dfc28dc09286fed06b Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 18:18:15 -0700 Subject: [PATCH 09/21] Use module level constants for float min/max --- Lib/test/test_math.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index a78e398c5ae1aa..251b28b657f6e6 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -16,6 +16,7 @@ INF = float('inf') NINF = float('-inf') FLOAT_MAX = sys.float_info.max +FLOAT_MIN = sys.float_info.min # detect evidence of double-rounding: fsum is not always correctly # rounded on machines that suffer from double rounding. @@ -780,14 +781,13 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN))) # Verify scaling for extremely large values - halfmax = sys.float_info.max / 2.0 + halfmax = FLOAT_MAX / 2.0 for n in range(10): self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) # Verify scaling for extremely small values - small = sys.float_info.min for exp in range(32): - scale = small / 2 ** exp + scale = FLOAT_MIN / 2.0 ** exp self.assertEqual(math.hypot(12*scale, 5*scale), 13*scale) def testLdexp(self): From 1a32ac74c722dab8644f904582c9508c2f58cb0f Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 18:20:22 -0700 Subject: [PATCH 10/21] Expand range for large value tests --- Lib/test/test_math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 251b28b657f6e6..d8fc22363eaab5 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -781,8 +781,8 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN))) # Verify scaling for extremely large values - halfmax = FLOAT_MAX / 2.0 - for n in range(10): + halfmax = FLOAT_MAX / 4.0 + for n in range(32): self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) # Verify scaling for extremely small values From 82a12e5b5a4e42ca3b82cec9dd017f07fa00cd97 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 18:25:38 -0700 Subject: [PATCH 11/21] Improve variable name --- Lib/test/test_math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index d8fc22363eaab5..0d04514fb35b59 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -781,9 +781,9 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN))) # Verify scaling for extremely large values - halfmax = FLOAT_MAX / 4.0 + fourthmax = FLOAT_MAX / 4.0 for n in range(32): - self.assertEqual(hypot(*([halfmax]*n)), halfmax * math.sqrt(n)) + self.assertEqual(hypot(*([fourthmax]*n)), fourthmax * math.sqrt(n)) # Verify scaling for extremely small values for exp in range(32): From 2d2e3aa2fa8f9e02178fff8392759c12da600fcb Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 20:57:00 -0700 Subject: [PATCH 12/21] Replace the existing hypot() code --- Lib/test/test_math.py | 21 ++--- Modules/clinic/mathmodule.c.h | 29 ------- Modules/mathmodule.c | 142 +++++++++------------------------- 3 files changed, 45 insertions(+), 147 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 0d04514fb35b59..30cda8a4b612dc 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -721,24 +721,13 @@ def testGcd(self): self.assertEqual(gcd(MyIndexable(120), MyIndexable(84)), 12) def testHypot(self): - self.assertRaises(TypeError, math.hypot) - self.ftest('hypot(0,0)', math.hypot(0,0), 0) - self.ftest('hypot(3,4)', math.hypot(3,4), 5) - self.assertEqual(math.hypot(NAN, INF), INF) - self.assertEqual(math.hypot(INF, NAN), INF) - self.assertEqual(math.hypot(NAN, NINF), INF) - self.assertEqual(math.hypot(NINF, NAN), INF) - self.assertRaises(OverflowError, math.hypot, FLOAT_MAX, FLOAT_MAX) - self.assertTrue(math.isnan(math.hypot(1.0, NAN))) - self.assertTrue(math.isnan(math.hypot(NAN, -2.0))) - - def test_multi_hypot(self): from decimal import Decimal from fractions import Fraction - hypot = math.mh + hypot = math.hypot - # Test different numbers of arguments from zero to five + # Test different numbers of arguments (from zero to five) + # against a straightforward pure python implementation args = math.e, math.pi, math.sqrt(2.0), math.gamma(3.5), math.sin(2.1) for i in range(len(args)+1): self.assertAlmostEqual( @@ -769,6 +758,9 @@ def test_multi_hypot(self): self.assertEqual(hypot(10, INF), INF) self.assertEqual(hypot(-10, INF), INF) self.assertEqual(hypot(NAN, INF), INF) + self.assertEqual(hypot(INF, NAN), INF) + self.assertEqual(hypot(NINF, NAN), INF) + self.assertEqual(hypot(NAN, NINF), INF) self.assertEqual(hypot(-INF, INF), INF) self.assertEqual(hypot(-INF, -INF), INF) self.assertEqual(hypot(10, -INF), INF) @@ -777,6 +769,7 @@ def test_multi_hypot(self): self.assertTrue(math.isnan(hypot(NAN))) self.assertTrue(math.isnan(hypot(0, NAN))) self.assertTrue(math.isnan(hypot(NAN, 10))) + self.assertTrue(math.isnan(hypot(10, NAN))) self.assertTrue(math.isnan(hypot(NAN, NAN))) self.assertTrue(math.isnan(hypot(NAN))) diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index ffebb079183cbf..67bff301b996b1 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -269,35 +269,6 @@ math_fmod(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -PyDoc_STRVAR(math_hypot__doc__, -"hypot($module, x, y, /)\n" -"--\n" -"\n" -"Return the Euclidean distance, sqrt(x*x + y*y)."); - -#define MATH_HYPOT_METHODDEF \ - {"hypot", (PyCFunction)math_hypot, METH_FASTCALL, math_hypot__doc__}, - -static PyObject * -math_hypot_impl(PyObject *module, double x, double y); - -static PyObject * -math_hypot(PyObject *module, PyObject *const *args, Py_ssize_t nargs) -{ - PyObject *return_value = NULL; - double x; - double y; - - if (!_PyArg_ParseStack(args, nargs, "dd:hypot", - &x, &y)) { - goto exit; - } - return_value = math_hypot_impl(module, x, y); - -exit: - return return_value; -} - PyDoc_STRVAR(math_pow__doc__, "pow($module, x, y, /)\n" "--\n" diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 68351f3d4c91fc..c5f4f790cbfee2 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -57,71 +57,6 @@ raised for division by zero and mod by zero. #include "clinic/mathmodule.c.h" -/* Notes: - - if (!Py_IS_FINITE(x)) { - if (Py_IS_NAN(x) || x > 0.0) - - ? FPE protection - - ? Don't scale if max == 0.0 - - ? If only one value, it is the hypot. - - Any infinity gives infinity - If no infinity, any NaN gives a NaN - - * hypot(x=1) -> TypeError // no keyword args - * hypot(-10.5) -> 10.5 // flip the sign to positive - hypot(-Inf) -> Inf; // flip the sign to positive - * hypot() -> 0.0 // degrade like sum([]) or reduce(hypot, sides, 0.0) - * hypot(0) -> 0.0 // don't divide by zero - * hypot(3, 4) // ints converted to floats - * hypot(D('3.0'), D('4.0')) -*/ - -/* AC: cannot convert yet, waiting for *args support */ -static PyObject * -m_hypot(PyObject *self, PyObject *args) -{ - Py_ssize_t i, n; - PyObject *item; - double max = 0.0; - double csum = 0.0; - double x; - int found_inf = 0; - int found_nan = 0; - - n = PyTuple_GET_SIZE(args); - for (i=0 ; i max) { - max = x; - } - } - if (found_inf) { - return PyFloat_FromDouble(max); - } - if (found_nan) { - return PyFloat_FromDouble(Py_NAN); - } - if (max == 0.0) { - return PyFloat_FromDouble(0.0); - } - for (i=0 ; i max) { + max = x; + } } - else if (Py_IS_INFINITY(r)) { - if (Py_IS_FINITE(x) && Py_IS_FINITE(y)) - errno = ERANGE; - else - errno = 0; + if (found_inf) { + return PyFloat_FromDouble(max); } - if (errno && is_error(r)) - return NULL; - else - return PyFloat_FromDouble(r); + if (found_nan) { + return PyFloat_FromDouble(Py_NAN); + } + if (max == 0.0) { + return PyFloat_FromDouble(0.0); + } + for (i=0 ; i Date: Wed, 25 Jul 2018 21:01:45 -0700 Subject: [PATCH 13/21] Clean stray whitespace --- Modules/mathmodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index c5f4f790cbfee2..ed32e8160357b7 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -57,7 +57,6 @@ raised for division by zero and mod by zero. #include "clinic/mathmodule.c.h" - /*[clinic input] module math [clinic start generated code]*/ From 9c25c65d570587f9665e0f65b0d282b1e2164a1a Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 22:13:52 -0700 Subject: [PATCH 14/21] Write the docstring --- Modules/mathmodule.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index ed32e8160357b7..2fe01d6d8d0ffc 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2074,6 +2074,21 @@ math_hypot(PyObject *self, PyObject *args) return PyFloat_FromDouble(max * sqrt(csum)); // XXX Handle overflow } +PyDoc_STRVAR(math_hypot_doc, + "hypot(*coordinates) -> value\n\n\ +Multidimensional Euclidean distance from the origin to a point.\n\ +\n\ +Roughly equivalent to:\n\ + sqrt(sum(x**2 for x in coordinates))\n\ +\n\ +For a two dimensional point (x, y), gives the hypotenuse\n\ +using the Pythagorean theorem: sqrt(x*x + y*y).\n\ +\n\ +For example, the hypotenuse of a 3/4/5 right triangle is:\n\ +\n\ + >>> hypot(3.0, 4.0)\n\ + 5.0\n\ +"); /* pow can't use math_2, but needs its own wrapper: the problem is that an infinite result can arise either as a result of overflow @@ -2345,7 +2360,7 @@ static PyMethodDef math_methods[] = { MATH_FSUM_METHODDEF {"gamma", math_gamma, METH_O, math_gamma_doc}, MATH_GCD_METHODDEF - {"hypot", math_hypot, METH_VARARGS, math_remainder_doc}, + {"hypot", math_hypot, METH_VARARGS, math_hypot_doc}, MATH_ISCLOSE_METHODDEF MATH_ISFINITE_METHODDEF MATH_ISINF_METHODDEF From c4dfbe575a91be712f1cfb92074fbf6278707ec2 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 22:37:14 -0700 Subject: [PATCH 15/21] Update the main documentation --- Doc/library/math.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Doc/library/math.rst b/Doc/library/math.rst index 33aec573a1f281..e60d02919c8f30 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -330,10 +330,19 @@ Trigonometric functions Return the cosine of *x* radians. -.. function:: hypot(x, y) +.. function:: hypot(*coordinates) - Return the Euclidean norm, ``sqrt(x*x + y*y)``. This is the length of the vector - from the origin to point ``(x, y)``. + Return the Euclidean norm, ``sqrt(sum(x**2 for x in coordinates))``. + This is the length of the vector from the origin to the point + given by the coordinates. + + For a two dimensional point ``(x, y)``, this is equivalent to computing + the hypotenuse of a right triangle using the Pythagorean theorem, + ``sqrt(x*x + y*y)``. + + .. versionchanged:: 3.8 + Added support for n-dimensional points. Formerly, only the two + dimensional case was supported. .. function:: sin(x) From 0734edd8927d5bbc760b5514f3c78f7f3c6077bc Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 25 Jul 2018 22:39:11 -0700 Subject: [PATCH 16/21] Add blurb --- .../NEWS.d/next/Library/2018-07-25-22-38-54.bpo-33089.C3CB7e.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2018-07-25-22-38-54.bpo-33089.C3CB7e.rst diff --git a/Misc/NEWS.d/next/Library/2018-07-25-22-38-54.bpo-33089.C3CB7e.rst b/Misc/NEWS.d/next/Library/2018-07-25-22-38-54.bpo-33089.C3CB7e.rst new file mode 100644 index 00000000000000..83152a77b20b55 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-07-25-22-38-54.bpo-33089.C3CB7e.rst @@ -0,0 +1 @@ +Enhanced math.hypot() to support more than two dimensions. From 43471a9a33a0439846c5f379dc0dacb44b1692bf Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 27 Jul 2018 00:00:45 -0700 Subject: [PATCH 17/21] Address Tim's review comments. * Made test for the zero argument case explicit. * Use exactly representable numbers in the extreme small value tests. * Guard against a malicious __float__ that succeeds on the first call and fails on the second. --- Lib/test/test_math.py | 3 ++- Modules/mathmodule.c | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 30cda8a4b612dc..2fac0b60bdac91 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -745,6 +745,7 @@ def testHypot(self): # Test corner cases self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero self.assertEqual(hypot(-10.5), 10.5) # Negative input + self.assertEqual(hypot(), 0.0) # Negative input # Test handling of bad arguments with self.assertRaises(TypeError): # Reject keyword args @@ -781,7 +782,7 @@ def testHypot(self): # Verify scaling for extremely small values for exp in range(32): scale = FLOAT_MIN / 2.0 ** exp - self.assertEqual(math.hypot(12*scale, 5*scale), 13*scale) + self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale) def testLdexp(self): self.assertRaises(TypeError, math.ldexp) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 2fe01d6d8d0ffc..0a3ae163199a54 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2068,7 +2068,11 @@ math_hypot(PyObject *self, PyObject *args) } for (i=0 ; i Date: Fri, 27 Jul 2018 00:24:39 -0700 Subject: [PATCH 18/21] Update argument clinic checksum --- Modules/clinic/mathmodule.c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 67bff301b996b1..3a487bdbea23b9 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -487,4 +487,4 @@ math_isclose(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject exit: return return_value; } -/*[clinic end generated code: output=e554bad553045546 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=1c35516a10443902 input=a9049054013a1b77]*/ From 8298444e8bf450734edb09ce474579b2a6946ca7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 27 Jul 2018 12:45:49 -0700 Subject: [PATCH 19/21] Reuse converted floats rather than converting twice --- Modules/mathmodule.c | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 0a3ae163199a54..b0a44ae488f1d6 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2037,45 +2037,51 @@ math_hypot(PyObject *self, PyObject *args) { Py_ssize_t i, n; PyObject *item; + double *coordinates; double max = 0.0; double csum = 0.0; - double x; + double x, result; int found_inf = 0; int found_nan = 0; n = PyTuple_GET_SIZE(args); + coordinates = (double *) PyObject_Malloc(n * sizeof(double)); for (i=0 ; i max) { max = x; } } if (found_inf) { - return PyFloat_FromDouble(max); + result = max; + goto done; } if (found_nan) { - return PyFloat_FromDouble(Py_NAN); + result = Py_NAN; + goto done; } if (max == 0.0) { - return PyFloat_FromDouble(0.0); + result = 0.0; + goto done; } for (i=0 ; i Date: Fri, 27 Jul 2018 19:59:23 -0700 Subject: [PATCH 20/21] Add test for a negative zero input --- Lib/test/test_math.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 2fac0b60bdac91..484389921a6916 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -746,6 +746,9 @@ def testHypot(self): self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero self.assertEqual(hypot(-10.5), 10.5) # Negative input self.assertEqual(hypot(), 0.0) # Negative input + self.assertEqual(1.0, + math.copysign(1.0, hypot(-0.0)) # Convert negative zero to positive zero + ) # Test handling of bad arguments with self.assertRaises(TypeError): # Reject keyword args From ee1374c67e1b2cdb071f6387dbf068ce084daf9d Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 27 Jul 2018 23:29:47 -0700 Subject: [PATCH 21/21] Test for failed malloc(). Only test inf at the end. --- Modules/mathmodule.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index b0a44ae488f1d6..726fc562129e46 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2041,11 +2041,12 @@ math_hypot(PyObject *self, PyObject *args) double max = 0.0; double csum = 0.0; double x, result; - int found_inf = 0; int found_nan = 0; n = PyTuple_GET_SIZE(args); coordinates = (double *) PyObject_Malloc(n * sizeof(double)); + if (coordinates == NULL) + return NULL; for (i=0 ; i max) { max = x; } } - if (found_inf) { + if (Py_IS_INFINITY(max)) { result = max; goto done; }