8000 bpo-33089: Multidimensional math.hypot() (GH-8474) · python/cpython@c6dabe3 · GitHub
[go: up one dir, main page]

Skip to content

Commit c6dabe3

Browse files
authored
bpo-33089: Multidimensional math.hypot() (GH-8474)
1 parent 5032692 commit c6dabe3

File tree

5 files changed

+143
-81
lines changed

5 files changed

+143
-81
lines changed

Doc/library/math.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
8000
@@ -330,10 +330,19 @@ Trigonometric functions
330330
Return the cosine of *x* radians.
331331

332332

333-
.. function:: hypot(x, y)
333+
.. function:: hypot(*coordinates)
334334

335-
Return the Euclidean norm, ``sqrt(x*x + y*y)``. This is the length of the vector
336-
from the origin to point ``(x, y)``.
335+
Return the Euclidean norm, ``sqrt(sum(x**2 for x in coordinates))``.
336+
This is the length of the vector from the origin to the point
337+
given by the coordinates.
338+
339+
For a two dimensional point ``(x, y)``, this is equivalent to computing
340+
the hypotenuse of a right triangle using the Pythagorean theorem,
341+
``sqrt(x*x + y*y)``.
342+
343+
.. versionchanged:: 3.8
344+
Added support for n-dimensional points. Formerly, only the two
345+
dimensional case was supported.
337346

338347

339348
.. function:: sin(x)

Lib/test/test_math.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
INF = float('inf')
1717
NINF = float('-inf')
1818
FLOAT_MAX = sys.float_info.max
19+
FLOAT_MIN = sys.float_info.min
1920

2021
# detect evidence of double-rounding: fsum is not always correctly
2122
# rounded on machines that suffer from double rounding.
@@ -720,16 +721,71 @@ def testGcd(self):
720721
self.assertEqual(gcd(MyIndexable(120), MyIndexable(84)), 12)
721722

722723
def testHypot(self):
723-
self.assertRaises(TypeError, math.hypot)
724-
self.ftest('hypot(0,0)', math.hypot(0,0), 0)
725-
self.ftest('hypot(3,4)', math.hypot(3,4), 5)
726-
self.assertEqual(math.hypot(NAN, INF), INF)
727-
self.assertEqual(math.hypot(INF, NAN), INF)
728-
self.assertEqual(math.hypot(NAN, NINF), INF)
729-
self.assertEqual(math.hypot(NINF, NAN), INF)
730-
self.assertRaises(OverflowError, math.hypot, FLOAT_MAX, FLOAT_MAX)
731-
self.assertTrue(math.isnan(math.hypot(1.0, NAN)))
732-
self.assertTrue(math.isnan(math.hypot(NAN, -2.0)))
724+
from decimal import Decimal
725+
from fractions import Fraction
726+
727+
hypot = math.hypot
728+
729+
# Test different numbers of arguments (from zero to five)
730+
# against a straightforward pure python implementation
731+
args = math.e, math.pi, math.sqrt(2.0), math.gamma(3.5), math.sin(2.1)
732+
for i in range(len(args)+1):
733+
self.assertAlmostEqual(
734+
hypot(*args[:i]),
735+
math.sqrt(sum(s**2 for s in args[:i]))
736+
)
737+
738+
# Test allowable types (those with __float__)
739+
self.assertEqual(hypot(12.0, 5.0), 13.0)
740+
self.assertEqual(hypot(12, 5), 13)
741+
self.assertEqual(hypot(Decimal(12), Decimal(5)), 13)
742+
self.assertEqual(hypot(Fraction(12, 32), Fraction(5, 32)), Fraction(13, 32))
743+
self.assertEqual(hypot(bool(1), bool(0), bool(1), bool(1)), math.sqrt(3))
744+
745+
# Test corner cases
746+
self.assertEqual(hypot(0.0, 0.0), 0.0) # Max input is zero
747+
self.assertEqual(hypot(-10.5), 10.5) # Negative input
748+
self.assertEqual(hypot(), 0.0) # Negative input
749+
self.assertEqual(1.0,
750+
math.copysign(1.0, hypot(-0.0)) # Convert negative zero to positive zero
751+
)
752+
753+
# Test handling of bad arguments
754+
with self.assertRaises(TypeError): # Reject keyword args
755+
hypot(x=1)
756+
with self.assertRaises(TypeError): # Reject values without __float__
757+
hypot(1.1, 'string', 2.2)
758+
759+
# Any infinity gives positive infinity.
760+
self.assertEqual(hypot(INF), INF)
761+
self.assertEqual(hypot(0, INF), INF)
762+
self.assertEqual(hypot(10, INF), INF)
763+
self.assertEqual(hypot(-10, INF), INF)
764+
self.assertEqual(hypot(NAN, INF), INF)
765+
self.assertEqual(hypot(INF, NAN), INF)
766+
self.assertEqual(hypot(NINF, NAN), INF)
767+
self.assertEqual(hypot(NAN, NINF), INF)
768+
self.assertEqual(hypot(-INF, INF), INF)
769+
self.assertEqual(hypot(-INF, -INF), INF)
770+
self.assertEqual(hypot(10, -INF), INF)
771+
772+
# If no infinity, any NaN gives a Nan.
773+
self.assertTrue(math.isnan(hypot(NAN)))
774+
self.assertTrue(math.isnan(hypot(0, NAN)))
775+
self.assertTrue(math.isnan(hypot(NAN, 10)))
776+
self.assertTrue(math.isnan(hypot(10, NAN)))
777+
self.assertTrue(math.isnan(hypot(NAN, NAN)))
778+
self.assertTrue(math.isnan(hypot(NAN)))
779+
780+
# Verify scaling for extremely large values
781+
fourthmax = FLOAT_MAX / 4.0
782+
for n in range(32):
783+
self.assertEqual(hypot(*([fourthmax]*n)), fourthmax * math.sqrt(n))
784+
785+
# Verify scaling for extremely small values
786+
for exp in range(32):
787+
scale = FLOAT_MIN / 2.0 ** exp
788+
self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale)
733789

734790
def testLdexp(self):
735791
self.assertRaises(TypeError, math.ldexp)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Enhanced math.hypot() to support more than two dimensions.

Modules/clinic/mathmodule.c.h

Lines changed: 1 addition & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/mathmodule.c

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,49 +2031,74 @@ math_fmod_impl(PyObject *module, double x, double y)
20312031
return PyFloat_FromDouble(r);
20322032
}
20332033

2034-
2035-
/*[clinic input]
2036-
math.hypot
2037-
2038-
x: double
2039-
y: double
2040-
/
2041-
2042-
Return the Euclidean distance, sqrt(x*x + y*y).
2043-
[clinic start generated code]*/
2044-
2034+
/* AC: cannot convert yet, waiting for *args support */
20452035
static PyObject *
2046-
math_hypot_impl(PyObject *module, double x, double y)
2047-
/*[clinic end generated code: output=b7686e5be468ef87 input=7f8eea70406474aa]*/
2036+
math_hypot(PyObject *self, PyObject *args)
20482037
{
2049-
double r;
2050-
/* hypot(x, +/-Inf) returns Inf, even if x is a NaN. */
2051-
if (Py_IS_INFINITY(x))
2052-
return PyFloat_FromDouble(fabs(x));
2053-
if (Py_IS_INFINITY(y))
2054-
return PyFloat_FromDouble(fabs(y));
2055-
errno = 0;
2056-
PyFPE_START_PROTECT("in math_hypot", return 0);
2057-
r = hypot(x, y);
2058-
PyFPE_END_PROTECT(r);
2059-
if (Py_IS_NAN(r)) {
2060-
if (!Py_IS_NAN(x) && !Py_IS_NAN(y))
2061-
errno = EDOM;
2062-
else
2063-
errno = 0;
2038+
Py_ssize_t i, n;
2039+
PyObject *item;
2040+
double *coordinates;
2041+
double max = 0.0;
2042+
double csum = 0.0;
2043+
double x, result;
2044+
int found_nan = 0;
2045+
2046+
n = PyTuple_GET_SIZE(args);
2047+
coordinates = (double *) PyObject_Malloc(n * sizeof(double));
2048+
if (coordinates == NULL)
2049+
return NULL;
2050+
for (i=0 ; i<n ; i++) {
2051+
item = PyTuple_GET_ITEM(args, i);
2052+
x = PyFloat_AsDouble(item);
2053+
if (x == -1.0 && PyErr_Occurred()) {
2054+
PyObject_Free(coordinates);
2055+
return NULL;
2056+
}
2057+
x = fabs(x);
2058+
coordinates[i] = x;
2059+
found_nan |= Py_IS_NAN(x);
2060+
if (x > max) {
2061+
max = x;
2062+
}
20642063
}
2065-
else if (Py_IS_INFINITY(r)) {
2066-
if (Py_IS_FINITE(x) && Py_IS_FINITE(y))
2067-
errno = ERANGE;
2068-
else
2069-
errno = 0;
2064+
if (Py_IS_INFINITY(max)) {
2065+
result = max;
2066+
goto done;
20702067
}
2071-
if (errno && is_error(r))
2072-
return NULL;
2073-
else
2074-
return PyFloat_FromDouble(r);
2068+
if (found_nan) {
2069+
result = Py_NAN;
2070+
goto done;
2071+
}
2072+
if (max == 0.0) {
2073+
result = 0.0;
2074+
goto done;
2075+
}
2076+
for (i=0 ; i<n ; i++) {
2077+
x = coordinates[i] / max;
2078+
csum += x * x;
2079+
}
2080+
result = max * sqrt(csum);
2081+
2082+
done:
2083+
PyObject_Free(coordinates);
2084+
return PyFloat_FromDouble(result);
20752085
}
20762086

2087+
PyDoc_STRVAR(math_hypot_doc,
2088+
"hypot(*coordinates) -> value\n\n\
2089+
Multidimensional Euclidean distance from the origin to a point.\n\
2090+
\n\
2091+
Roughly equivalent to:\n\
2092+
sqrt(sum(x**2 for x in coordinates))\n\
2093+
\n\
2094+
For a two dimensional point (x, y), gives the hypotenuse\n\
2095+
using the Pythagorean theorem: sqrt(x*x + y*y).\n\
2096+
\n\
2097+
For example, the hypotenuse of a 3/4/5 right triangle is:\n\
2098+
\n\
2099+
>>> hypot(3.0, 4.0)\n\
2100+
5.0\n\
2101+
");
20772102

20782103
/* pow can't use math_2, but needs its own wrapper: the problem is
20792104
that an infinite result can arise either as a result of overflow
@@ -2345,7 +2370,7 @@ static PyMethodDef math_methods[] = {
23452370
MATH_FSUM_METHODDEF
23462371
{"gamma", math_gamma, METH_O, math_gamma_doc},
23472372
MATH_GCD_METHODDEF
2348-
MATH_HYPOT_METHODDEF
2373+
{"hypot", math_hypot, METH_VARARGS, math_hypot_doc},
23492374
MATH_ISCLOSE_METHODDEF
23502375
MATH_ISFINITE_METHODDEF
23512376
MATH_ISINF_METHODDEF

0 commit comments

Comments
 (0)
0