bpo-33089: Add math.dist() for computing the Euclidean distance between two points (GH-8561)
This commit is contained in:
parent
9d5727326a
commit
9c18b1ae52
|
@ -330,6 +330,18 @@ Trigonometric functions
|
||||||
Return the cosine of *x* radians.
|
Return the cosine of *x* radians.
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: dist(p, q)
|
||||||
|
|
||||||
|
Return the Euclidean distance between two points *p* and *q*, each
|
||||||
|
given as a tuple of coordinates. The two tuples must be the same size.
|
||||||
|
|
||||||
|
Roughly equivalent to::
|
||||||
|
|
||||||
|
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
|
||||||
|
|
||||||
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
|
|
||||||
.. function:: hypot(*coordinates)
|
.. function:: hypot(*coordinates)
|
||||||
|
|
||||||
Return the Euclidean norm, ``sqrt(sum(x**2 for x in coordinates))``.
|
Return the Euclidean norm, ``sqrt(sum(x**2 for x in coordinates))``.
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
from test.support import run_unittest, verbose, requires_IEEE_754
|
from test.support import run_unittest, verbose, requires_IEEE_754
|
||||||
from test import support
|
from test import support
|
||||||
import unittest
|
import unittest
|
||||||
|
import itertools
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import random
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
@ -787,6 +789,107 @@ class MathTests(unittest.TestCase):
|
||||||
scale = FLOAT_MIN / 2.0 ** exp
|
scale = FLOAT_MIN / 2.0 ** exp
|
||||||
self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale)
|
self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale)
|
||||||
|
|
||||||
|
def testDist(self):
|
||||||
|
from decimal import Decimal as D
|
||||||
|
from fractions import Fraction as F
|
||||||
|
|
||||||
|
dist = math.dist
|
||||||
|
sqrt = math.sqrt
|
||||||
|
|
||||||
|
# Simple exact case
|
||||||
|
self.assertEqual(dist((1, 2, 3), (4, 2, -1)), 5.0)
|
||||||
|
|
||||||
|
# Test different numbers of arguments (from zero to nine)
|
||||||
|
# against a straightforward pure python implementation
|
||||||
|
for i in range(9):
|
||||||
|
for j in range(5):
|
||||||
|
p = tuple(random.uniform(-5, 5) for k in range(i))
|
||||||
|
q = tuple(random.uniform(-5, 5) for k in range(i))
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
dist(p, q),
|
||||||
|
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test allowable types (those with __float__)
|
||||||
|
self.assertEqual(dist((14.0, 1.0), (2.0, -4.0)), 13.0)
|
||||||
|
self.assertEqual(dist((14, 1), (2, -4)), 13)
|
||||||
|
self.assertEqual(dist((D(14), D(1)), (D(2), D(-4))), D(13))
|
||||||
|
self.assertEqual(dist((F(14, 32), F(1, 32)), (F(2, 32), F(-4, 32))),
|
||||||
|
F(13, 32))
|
||||||
|
self.assertEqual(dist((True, True, False, True, False),
|
||||||
|
(True, False, True, True, False)),
|
||||||
|
sqrt(2.0))
|
||||||
|
|
||||||
|
# Test corner cases
|
||||||
|
self.assertEqual(dist((13.25, 12.5, -3.25),
|
||||||
|
(13.25, 12.5, -3.25)),
|
||||||
|
0.0) # Distance with self is zero
|
||||||
|
self.assertEqual(dist((), ()), 0.0) # Zero-dimensional case
|
||||||
|
self.assertEqual(1.0, # Convert negative zero to positive zero
|
||||||
|
math.copysign(1.0, dist((-0.0,), (0.0,)))
|
||||||
|
)
|
||||||
|
self.assertEqual(1.0, # Convert negative zero to positive zero
|
||||||
|
math.copysign(1.0, dist((0.0,), (-0.0,)))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify tuple subclasses are allowed
|
||||||
|
class T(tuple): # tuple subclas
|
||||||
|
pass
|
||||||
|
self.assertEqual(dist(T((1, 2, 3)), ((4, 2, -1))), 5.0)
|
||||||
|
|
||||||
|
# Test handling of bad arguments
|
||||||
|
with self.assertRaises(TypeError): # Reject keyword args
|
||||||
|
dist(p=(1, 2, 3), q=(4, 5, 6))
|
||||||
|
with self.assertRaises(TypeError): # Too few args
|
||||||
|
dist((1, 2, 3))
|
||||||
|
with self.assertRaises(TypeError): # Too many args
|
||||||
|
dist((1, 2, 3), (4, 5, 6), (7, 8, 9))
|
||||||
|
with self.assertRaises(TypeError): # Scalars not allowed
|
||||||
|
dist(1, 2)
|
||||||
|
with self.assertRaises(TypeError): # Lists not allowed
|
||||||
|
dist([1, 2, 3], [4, 5, 6])
|
||||||
|
with self.assertRaises(TypeError): # Reject values without __float__
|
||||||
|
dist((1.1, 'string', 2.2), (1, 2, 3))
|
||||||
|
with self.assertRaises(ValueError): # Check dimension agree
|
||||||
|
dist((1, 2, 3, 4), (5, 6, 7))
|
||||||
|
with self.assertRaises(ValueError): # Check dimension agree
|
||||||
|
dist((1, 2, 3), (4, 5, 6, 7))
|
||||||
|
|
||||||
|
|
||||||
|
# Verify that the one dimensional case equivalent to abs()
|
||||||
|
for i in range(20):
|
||||||
|
p, q = random.random(), random.random()
|
||||||
|
self.assertEqual(dist((p,), (q,)), abs(p - q))
|
||||||
|
|
||||||
|
# Test special values
|
||||||
|
values = [NINF, -10.5, -0.0, 0.0, 10.5, INF, NAN]
|
||||||
|
for p in itertools.product(values, repeat=3):
|
||||||
|
for q in itertools.product(values, repeat=3):
|
||||||
|
diffs = [px - qx for px, qx in zip(p, q)]
|
||||||
|
if any(map(math.isinf, diffs)):
|
||||||
|
# Any infinite difference gives positive infinity.
|
||||||
|
self.assertEqual(dist(p, q), INF)
|
||||||
|
elif any(map(math.isnan, diffs)):
|
||||||
|
# If no infinity, any NaN gives a Nan.
|
||||||
|
self.assertTrue(math.isnan(dist(p, q)))
|
||||||
|
|
||||||
|
# Verify scaling for extremely large values
|
||||||
|
fourthmax = FLOAT_MAX / 4.0
|
||||||
|
for n in range(32):
|
||||||
|
p = (fourthmax,) * n
|
||||||
|
q = (0.0,) * n
|
||||||
|
self.assertEqual(dist(p, q), fourthmax * math.sqrt(n))
|
||||||
|
self.assertEqual(dist(q, p), fourthmax * math.sqrt(n))
|
||||||
|
|
||||||
|
# Verify scaling for extremely small values
|
||||||
|
for exp in range(32):
|
||||||
|
scale = FLOAT_MIN / 2.0 ** exp
|
||||||
|
p = (4*scale, 3*scale)
|
||||||
|
q = (0.0, 0.0)
|
||||||
|
self.assertEqual(math.dist(p, q), 5*scale)
|
||||||
|
self.assertEqual(math.dist(q, p), 5*scale)
|
||||||
|
|
||||||
|
|
||||||
def testLdexp(self):
|
def testLdexp(self):
|
||||||
self.assertRaises(TypeError, math.ldexp)
|
self.assertRaises(TypeError, math.ldexp)
|
||||||
self.ftest('ldexp(0,1)', math.ldexp(0,1), 0)
|
self.ftest('ldexp(0,1)', math.ldexp(0,1), 0)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add math.dist() to compute the Euclidean distance between two points.
|
|
@ -269,6 +269,41 @@ exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyDoc_STRVAR(math_dist__doc__,
|
||||||
|
"dist($module, p, q, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n"
|
||||||
|
"Return the Euclidean distance between two points p and q.\n"
|
||||||
|
"\n"
|
||||||
|
"The points should be specified as tuples of coordinates.\n"
|
||||||
|
"Both tuples must be the same size.\n"
|
||||||
|
"\n"
|
||||||
|
"Roughly equivalent to:\n"
|
||||||
|
" sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))");
|
||||||
|
|
||||||
|
#define MATH_DIST_METHODDEF \
|
||||||
|
{"dist", (PyCFunction)math_dist, METH_FASTCALL, math_dist__doc__},
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
math_dist_impl(PyObject *module, PyObject *p, PyObject *q);
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
math_dist(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
|
||||||
|
{
|
||||||
|
PyObject *return_value = NULL;
|
||||||
|
PyObject *p;
|
||||||
|
PyObject *q;
|
||||||
|
|
||||||
|
if (!_PyArg_ParseStack(args, nargs, "O!O!:dist",
|
||||||
|
&PyTuple_Type, &p, &PyTuple_Type, &q)) {
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
return_value = math_dist_impl(module, p, q);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
return return_value;
|
||||||
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(math_pow__doc__,
|
PyDoc_STRVAR(math_pow__doc__,
|
||||||
"pow($module, x, y, /)\n"
|
"pow($module, x, y, /)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
|
@ -487,4 +522,4 @@ math_isclose(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject
|
||||||
exit:
|
exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
/*[clinic end generated code: output=1c35516a10443902 input=a9049054013a1b77]*/
|
/*[clinic end generated code: output=d936137c1189b89b input=a9049054013a1b77]*/
|
||||||
|
|
|
@ -2031,6 +2031,89 @@ math_fmod_impl(PyObject *module, double x, double y)
|
||||||
return PyFloat_FromDouble(r);
|
return PyFloat_FromDouble(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
math.dist
|
||||||
|
|
||||||
|
p: object(subclass_of='&PyTuple_Type')
|
||||||
|
q: object(subclass_of='&PyTuple_Type')
|
||||||
|
/
|
||||||
|
|
||||||
|
Return the Euclidean distance between two points p and q.
|
||||||
|
|
||||||
|
The points should be specified as tuples of coordinates.
|
||||||
|
Both tuples must be the same size.
|
||||||
|
|
||||||
|
Roughly equivalent to:
|
||||||
|
sqrt(sum((px - qx) ** 2.0 for px, qx in zip(p, q)))
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
math_dist_impl(PyObject *module, PyObject *p, PyObject *q)
|
||||||
|
/*[clinic end generated code: output=56bd9538d06bbcfe input=937122eaa5f19272]*/
|
||||||
|
{
|
||||||
|
PyObject *item;
|
||||||
|
double *diffs;
|
||||||
|
double max = 0.0;
|
||||||
|
double csum = 0.0;
|
||||||
|
double x, px, qx, result;
|
||||||
|
Py_ssize_t i, m, n;
|
||||||
|
int found_nan = 0;
|
||||||
|
|
||||||
|
m = PyTuple_GET_SIZE(p);
|
||||||
|
n = PyTuple_GET_SIZE(q);
|
||||||
|
if (m != n) {
|
||||||
|
PyErr_SetString(PyExc_ValueError,
|
||||||
|
"both points must have the same number of dimensions");
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
}
|
||||||
|
diffs = (double *) PyObject_Malloc(n * sizeof(double));
|
||||||
|
if (diffs == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (i=0 ; i<n ; i++) {
|
||||||
|
item = PyTuple_GET_ITEM(p, i);
|
||||||
|
px = PyFloat_AsDouble(item);
|
||||||
|
if (px == -1.0 && PyErr_Occurred()) {
|
||||||
|
PyObject_Free(diffs);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
item = PyTuple_GET_ITEM(q, i);
|
||||||
|
qx = PyFloat_AsDouble(item);
|
||||||
|
if (qx == -1.0 && PyErr_Occurred()) {
|
||||||
|
PyObject_Free(diffs);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
x = fabs(px - qx);
|
||||||
|
diffs[i] = x;
|
||||||
|
found_nan |= Py_IS_NAN(x);
|
||||||
|
if (x > max) {
|
||||||
|
max = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Py_IS_INFINITY(max)) {
|
||||||
|
result = max;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (found_nan) {
|
||||||
|
result = Py_NAN;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
if (max == 0.0) {
|
||||||
|
result = 0.0;
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
for (i=0 ; i<n ; i++) {
|
||||||
|
x = diffs[i] / max;
|
||||||
|
csum += x * x;
|
||||||
|
}
|
||||||
|
result = max * sqrt(csum);
|
||||||
|
|
||||||
|
done:
|
||||||
|
PyObject_Free(diffs);
|
||||||
|
return PyFloat_FromDouble(result);
|
||||||
|
}
|
||||||
|
|
||||||
/* AC: cannot convert yet, waiting for *args support */
|
/* AC: cannot convert yet, waiting for *args support */
|
||||||
static PyObject *
|
static PyObject *
|
||||||
math_hypot(PyObject *self, PyObject *args)
|
math_hypot(PyObject *self, PyObject *args)
|
||||||
|
@ -2358,6 +2441,7 @@ static PyMethodDef math_methods[] = {
|
||||||
{"cos", math_cos, METH_O, math_cos_doc},
|
{"cos", math_cos, METH_O, math_cos_doc},
|
||||||
{"cosh", math_cosh, METH_O, math_cosh_doc},
|
{"cosh", math_cosh, METH_O, math_cosh_doc},
|
||||||
MATH_DEGREES_METHODDEF
|
MATH_DEGREES_METHODDEF
|
||||||
|
MATH_DIST_METHODDEF
|
||||||
{"erf", math_erf, METH_O, math_erf_doc},
|
{"erf", math_erf, METH_O, math_erf_doc},
|
||||||
{"erfc", math_erfc, METH_O, math_erfc_doc},
|
{"erfc", math_erfc, METH_O, math_erfc_doc},
|
||||||
{"exp", math_exp, METH_O, math_exp_doc},
|
{"exp", math_exp, METH_O, math_exp_doc},
|
||||||
|
|
Loading…
Reference in New Issue