Skip to content

Commit 9c18b1a

Browse files
authored
bpo-33089: Add math.dist() for computing the Euclidean distance between two points (GH-8561)
1 parent 9d57273 commit 9c18b1a

File tree

5 files changed

+236
-1
lines changed

5 files changed

+236
-1
lines changed

Doc/library/math.rst

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

332332

333+
.. function:: dist(p, q)
334+
335+
Return the Euclidean distance between two points *p* and *q*, each
336+
given as a tuple of coordinates. The two tuples must be the same size.
337+
338+
Roughly equivalent to::
339+
340+
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
341+
342+
.. versionadded:: 3.8
343+
344+
333345
.. function:: hypot(*coordinates)
334346

335347
Return the Euclidean norm, ``sqrt(sum(x**2 for x in coordinates))``.

Lib/test/test_math.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from test.support import run_unittest, verbose, requires_IEEE_754
55
from test import support
66
import unittest
7+
import itertools
78
import math
89
import os
910
import platform
11+
import random
1012
import struct
1113
import sys
1214
import sysconfig
@@ -787,6 +789,107 @@ def testHypot(self):
787789
scale = FLOAT_MIN / 2.0 ** exp
788790
self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale)
789791

792+
def testDist(self):
793+
from decimal import Decimal as D
794+
from fractions import Fraction as F
795+
796+
dist = math.dist
797+
sqrt = math.sqrt
798+
799+
# Simple exact case
800+
self.assertEqual(dist((1, 2, 3), (4, 2, -1)), 5.0)
801+
802+
# Test different numbers of arguments (from zero to nine)
803+
# against a straightforward pure python implementation
804+
for i in range(9):
805+
for j in range(5):
806+
p = tuple(random.uniform(-5, 5) for k in range(i))
807+
q = tuple(random.uniform(-5, 5) for k in range(i))
808+
self.assertAlmostEqual(
809+
dist(p, q),
810+
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
811+
)
812+
813+
# Test allowable types (those with __float__)
814+
self.assertEqual(dist((14.0, 1.0), (2.0, -4.0)), 13.0)
815+
self.assertEqual(dist((14, 1), (2, -4)), 13)
816+
self.assertEqual(dist((D(14), D(1)), (D(2), D(-4))), D(13))
817+
self.assertEqual(dist((F(14, 32), F(1, 32)), (F(2, 32), F(-4, 32))),
818+
F(13, 32))
819+
self.assertEqual(dist((True, True, False, True, False),
820+
(True, False, True, True, False)),
821+
sqrt(2.0))
822+
823+
# Test corner cases
824+
self.assertEqual(dist((13.25, 12.5, -3.25),
825+
(13.25, 12.5, -3.25)),
826+
0.0) # Distance with self is zero
827+
self.assertEqual(dist((), ()), 0.0) # Zero-dimensional case
828+
self.assertEqual(1.0, # Convert negative zero to positive zero
829+
math.copysign(1.0, dist((-0.0,), (0.0,)))
830+
)
831+
self.assertEqual(1.0, # Convert negative zero to positive zero
832+
math.copysign(1.0, dist((0.0,), (-0.0,)))
833+
)
834+
835+
# Verify tuple subclasses are allowed
836+
class T(tuple): # tuple subclas
837+
pass
838+
self.assertEqual(dist(T((1, 2, 3)), ((4, 2, -1))), 5.0)
839+
840+
# Test handling of bad arguments
841+
with self.assertRaises(TypeError): # Reject keyword args
842+
dist(p=(1, 2, 3), q=(4, 5, 6))
843+
with self.assertRaises(TypeError): # Too few args
844+
dist((1, 2, 3))
845+
with self.assertRaises(TypeError): # Too many args
846+
dist((1, 2, 3), (4, 5, 6), (7, 8, 9))
847+
with self.assertRaises(TypeError): # Scalars not allowed
848+
dist(1, 2)
849+
with self.assertRaises(TypeError): # Lists not allowed
850+
dist([1, 2, 3], [4, 5, 6])
851+
with self.assertRaises(TypeError): # Reject values without __float__
852+
dist((1.1, 'string', 2.2), (1, 2, 3))
853+
with self.assertRaises(ValueError): # Check dimension agree
854+
dist((1, 2, 3, 4), (5, 6, 7))
855+
with self.assertRaises(ValueError): # Check dimension agree
856+
dist((1, 2, 3), (4, 5, 6, 7))
857+
858+
859+
# Verify that the one dimensional case equivalent to abs()
860+
for i in range(20):
861+
p, q = random.random(), random.random()
862+
self.assertEqual(dist((p,), (q,)), abs(p - q))
863+
864+
# Test special values
865+
values = [NINF, -10.5, -0.0, 0.0, 10.5, INF, NAN]
866+
for p in itertools.product(values, repeat=3):
867+
for q in itertools.product(values, repeat=3):
868+
diffs = [px - qx for px, qx in zip(p, q)]
869+
if any(map(math.isinf, diffs)):
870+
# Any infinite difference gives positive infinity.
871+
self.assertEqual(dist(p, q), INF)
872+
elif any(map(math.isnan, diffs)):
873+
# If no infinity, any NaN gives a Nan.
874+
self.assertTrue(math.isnan(dist(p, q)))
875+
876+
# Verify scaling for extremely large values
877+
fourthmax = FLOAT_MAX / 4.0
878+
for n in range(32):
879+
p = (fourthmax,) * n
880+
q = (0.0,) * n
881+
self.assertEqual(dist(p, q), fourthmax * math.sqrt(n))
882+
self.assertEqual(dist(q, p), fourthmax * math.sqrt(n))
883+
884+
# Verify scaling for extremely small values
885+
for exp in range(32):
886+
scale = FLOAT_MIN / 2.0 ** exp
887+
p = (4*scale, 3*scale)
888+
q = (0.0, 0.0)
889+
self.assertEqual(math.dist(p, q), 5*scale)
890+
self.assertEqual(math.dist(q, p), 5*scale)
891+
892+
790893
def testLdexp(self):
791894
self.assertRaises(TypeError, math.ldexp)
792895
self.ftest('ldexp(0,1)', math.ldexp(0,1), 0)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add math.dist() to compute the Euclidean distance between two points.

Modules/clinic/mathmodule.c.h

Lines changed: 36 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/mathmodule.c

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

2034+
/*[clinic input]
2035+
math.dist
2036+
2037+
p: object(subclass_of='&PyTuple_Type')
2038+
q: object(subclass_of='&PyTuple_Type')
2039+
/
2040+
2041+
Return the Euclidean distance between two points p and q.
2042+
2043+
The points should be specified as tuples of coordinates.
2044+
Both tuples must be the same size.
2045+
2046+
Roughly equivalent to:
2047+
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
2048+
[clinic start generated code]*/
2049+
2050+
static PyObject *
2051+
math_dist_impl(PyObject *module, PyObject *p, PyObject *q)
2052+
/*[clinic end generated code: output=56bd9538d06bbcfe input=937122eaa5f19272]*/
2053+
{
2054+
PyObject *item;
2055+
double *diffs;
2056+
double max = 0.0;
2057+
double csum = 0.0;
2058+
double x, px, qx, result;
2059+
Py_ssize_t i, m, n;
2060+
int found_nan = 0;
2061+
2062+
m = PyTuple_GET_SIZE(p);
2063+
n = PyTuple_GET_SIZE(q);
2064+
if (m != n) {
2065+
PyErr_SetString(PyExc_ValueError,
2066+
"both points must have the same number of dimensions");
2067+
return NULL;
2068+
2069+
}
2070+
diffs = (double *) PyObject_Malloc(n * sizeof(double));
2071+
if (diffs == NULL) {
2072+
return NULL;
2073+
}
2074+
for (i=0 ; i<n ; i++) {
2075+
item = PyTuple_GET_ITEM(p, i);
2076+
px = PyFloat_AsDouble(item);
2077+
if (px == -1.0 && PyErr_Occurred()) {
2078+
PyObject_Free(diffs);
2079+
return NULL;
2080+
}
2081+
item = PyTuple_GET_ITEM(q, i);
2082+
qx = PyFloat_AsDouble(item);
2083+
if (qx == -1.0 && PyErr_Occurred()) {
2084+
PyObject_Free(diffs);
2085+
return NULL;
2086+
}
2087+
x = fabs(px - qx);
2088+
diffs[i] = x;
2089+
found_nan |= Py_IS_NAN(x);
2090+
if (x > max) {
2091+
max = x;
2092+
}
2093+
}
2094+
if (Py_IS_INFINITY(max)) {
2095+
result = max;
2096+
goto done;
2097+
}
2098+
if (found_nan) {
2099+
result = Py_NAN;
2100+
goto done;
2101+
}
2102+
if (max == 0.0) {
2103+
result = 0.0;
2104+
goto done;
2105+
}
2106+
for (i=0 ; i<n ; i++) {
2107+
x = diffs[i] / max;
2108+
csum += x * x;
2109+
}
2110+
result = max * sqrt(csum);
2111+
2112+
done:
2113+
PyObject_Free(diffs);
2114+
return PyFloat_FromDouble(result);
2115+
}
2116+
20342117
/* AC: cannot convert yet, waiting for *args support */
20352118
static PyObject *
20362119
math_hypot(PyObject *self, PyObject *args)
@@ -2358,6 +2441,7 @@ static PyMethodDef math_methods[] = {
23582441
{"cos", math_cos, METH_O, math_cos_doc},
23592442
{"cosh", math_cosh, METH_O, math_cosh_doc},
23602443
MATH_DEGREES_METHODDEF
2444+
MATH_DIST_METHODDEF
23612445
{"erf", math_erf, METH_O, math_erf_doc},
23622446
{"erfc", math_erfc, METH_O, math_erfc_doc},
23632447
{"exp", math_exp, METH_O, math_exp_doc},

0 commit comments

Comments
 (0)