Repository: scipy/scipy

Issue:
scipy.spatial.SphericalVoronoi gives incorrect results
When all of the points are in one hemisphere some of the Voronoi points are incorrect.

To be more precise, certain of the points are on the wrong side of the sphere.  An example is to create a tetrahedron with the following generator points:


generators (polar)
 [[-25.56198973  10.81015471]
 [-54.40661695  26.73672285]
 [-55.29180524  -6.7928405 ]
 [-45.12839961   9.12243389]]
generators (rectangular)
 [[ 0.88610999 -0.42383021  0.18755541]
 [ 0.51980039 -0.72622668  0.4498915 ]
 [ 0.56540011 -0.81629197 -0.11827989]
 [ 0.69659682 -0.69972598  0.15854467]]

![p](https://user-images.githubusercontent.com/2798381/47086102-7b2f5300-d1cd-11e8-9394-405ca519bca6.png)

The first three Voronoi points and associated Delaunay triangles come out fine:
Voronoi vertex number 1 , coordinates: [ 0.73647759 -0.54764287  0.39709955]
Delaunay vertices
 [[ 0.69659682 -0.69972598  0.15854467]
 [ 0.88610999 -0.42383021  0.18755541]
 [ 0.51980039 -0.72622668  0.4498915 ]]
Voronoi vertex number 2 , coordinates: [ 0.42155897 -0.88977832  0.17487877]
Delaunay vertices
 [[ 0.69659682 -0.69972598  0.15854467]
 [ 0.56540011 -0.81629197 -0.11827989]
 [ 0.51980039 -0.72622668  0.4498915 ]]
Voronoi vertex number 3 , coordinates: [ 0.82153008 -0.54761634 -0.15875979]
Delaunay vertices
 [[ 0.69659682 -0.69972598  0.15854467]
 [ 0.56540011 -0.81629197 -0.11827989]
 [ 0.88610999 -0.42383021  0.18755541]]

![v1](https://user-images.githubusercontent.com/2798381/47086240-ce090a80-d1cd-11e8-83be-95237aff6c81.png)
![v2](https://user-images.githubusercontent.com/2798381/47086249-d2352800-d1cd-11e8-8215-32c9c047f499.png)
![v3](https://user-images.githubusercontent.com/2798381/47086267-de20ea00-d1cd-11e8-945c-1280f73d67a4.png)

The fourth point is not correct - it should be on the opposite side of the sphere:
Voronoi vertex number 0 , coordinates: [ 0.69593711 -0.69851441  0.16658079]
Delaunay vertices
 [[ 0.56540011 -0.81629197 -0.11827989]
 [ 0.88610999 -0.42383021  0.18755541]
 [ 0.51980039 -0.72622668  0.4498915 ]]

![v0](https://user-images.githubusercontent.com/2798381/47086358-17f1f080-d1ce-11e8-862c-1902bb002aca.png)

I have a good understanding of the algorithm and what the problem is but I don't know how to fix it using qhull.



Discussion:
Related / duplicate of #8859 & also mentioned in #5232.

I've read a few of the algorithm papers in the (often recent) literature, and haven't seen one mention a proof for a working algorithm with single hemisphere input.

> I have a good understanding of the algorithm and what the problem is but I don't know how to fix it using qhull.

`SphericalVoronoi` is not implemented using `qhull` under the hood, unlike many other components of the computational geometry infrastructure in SciPy. It is mostly Python with a bit of Cython mixed in.

I suspect a robust PR or suggestion of a citation we could follow to solve this issue would be most welcome. cc @WWmore, @fietew, and @niknow.

We may have to be a little cautious if the algorithm isn't published in some form though--I think SciPy generally aims for robust / established algorithms (indeed, most of the Spherical Voronoi papers probably just barely pass that threshold).
That said, we probably do call 1 or 2 qhull functions for things like convex hull or similar in the current algorithm
Sorry about the duplicate issue.  I did search but maybe my github skills are a little weak.

I thought the spherical Voronoi algorithm uses scipy.spatial.ConvexHull to compute the convex hull, and the documentation for that algorithm clearly says it uses QHull to compute the convex hull.  BTW, I am not a geometer.  My math and computer skills are pretty good and I have done a lot of reading on this topic but I am certainly not a professional geometer.

The problem is that ConvexHull doesn't produce the right convex hull.  It computes the hull as a set of points in 3-space, but the convex hull as discussed in Caroli and the other references is a set of regions on the sphere.  The 3d hull can give you the spherical hull but you need to partition the 3d hull into the outer hull and the inner hull (this is the part I don't know how to do).  Projecting the circumcenters of the outer hull onto the sphere as it is currently done gives the "correct" part of the tesselation.  Projecting the circumcenters of the inner hull to the OPPOSITE side of the sphere gives the correct tesselation.

The Ph.D. thesis by Kevin Q. Brown, "Geometric Transforms for Fast Geometric Algorithms", 1979 and the masters thesis "Construction of Delaunay Triangulations on the Sphere: A Parallel Approach" by Veronica Larrea (especially the latter) give algorithms, although proofs are lacking in both cases.

Just posted as you were posting.  Unless the code has changed since I downloaded it, it calls ConvexHull to do the hull.  
@rtshort Yeah, you're right about that -- we didn't re-write the convex hull algorithm from scratch :)

Those are some interesting insights! The Brown thesis is indeed pretty famous, but your suggestions about splitting the 3D hull into an outer and inner hull are intriguing!
The Larrea thesis was more insightful for me.  That was where I got the aha! about the spherical hull vs. the 3d hull and the notion of inner and outer hull.  The notion of using the inner and outer 3d hulls is mine (although I doubt it is original).  It is a lot easier to visualize in 2d.  Looking at the following picture I have three points on the circle (2-sphere).  The figure to the right is the 2d convex hull, i.e. the hull computed by taking the points in 2-space.  The figure on the bottom shows the convex hull on the circle, in which each set of points defines an arc of the hull (colored).  The center of each arc are the Voronoi points and can be found by projecting from the 2d hull segments to the circle, but the key is that the inner hull (always a single segment in a 2-sphere) projects to the opposite side.   I sketched out a proof for this, but have not tried to fill it in.

All I wanted was to find a point in a geographic region that was furthest from the boundaries.  Just for my own edification.  Instead I spend many weeks learning computational geometry!  Actually pretty interesting and fun in a geeky sort of way.

![bug](https://user-images.githubusercontent.com/2798381/47168274-531b1f00-d2b5-11e8-8d87-fbf3ce708262.png)


Resolving patch:
diff --git a/scipy/spatial/_spherical_voronoi.py b/scipy/spatial/_spherical_voronoi.py
--- a/scipy/spatial/_spherical_voronoi.py
+++ b/scipy/spatial/_spherical_voronoi.py
@@ -11,6 +11,7 @@
 # Distributed under the same BSD license as SciPy.
 #
 
+import warnings
 import numpy as np
 import scipy
 from . import _voronoi
@@ -18,6 +19,7 @@
 
 __all__ = ['SphericalVoronoi']
 
+
 def sphere_check(points, radius, center):
     """ Determines distance of generators from theoretical sphere
     surface.
@@ -29,65 +31,6 @@ def sphere_check(points, radius, center):
     max_discrepancy = (np.sqrt(actual_squared_radii) - radius).max()
     return abs(max_discrepancy)
 
-def calc_circumcenters(tetrahedrons):
-    """ Calculates the cirumcenters of the circumspheres of tetrahedrons.
-
-    An implementation based on
-    http://mathworld.wolfram.com/Circumsphere.html
-
-    Parameters
-    ----------
-    tetrahedrons : an array of shape (N, 4, 3)
-        consisting of N tetrahedrons defined by 4 points in 3D
-
-    Returns
-    ----------
-    circumcenters : an array of shape (N, 3)
-        consisting of the N circumcenters of the tetrahedrons in 3D
-
-    """
-
-    num = tetrahedrons.shape[0]
-    a = np.concatenate((tetrahedrons, np.ones((num, 4, 1))), axis=2)
-
-    sums = np.sum(tetrahedrons ** 2, axis=2)
-    d = np.concatenate((sums[:, :, np.newaxis], a), axis=2)
-
-    dx = np.delete(d, 1, axis=2)
-    dy = np.delete(d, 2, axis=2)
-    dz = np.delete(d, 3, axis=2)
-
-    dx = np.linalg.det(dx)
-    dy = -np.linalg.det(dy)
-    dz = np.linalg.det(dz)
-    a = np.linalg.det(a)
-
-    nominator = np.vstack((dx, dy, dz))
-    denominator = 2*a
-    return (nominator / denominator).T
-
-
-def project_to_sphere(points, center, radius):
-    """
-    Projects the elements of points onto the sphere defined
-    by center and radius.
-
-    Parameters
-    ----------
-    points : array of floats of shape (npoints, ndim)
-             consisting of the points in a space of dimension ndim
-    center : array of floats of shape (ndim,)
-            the center of the sphere to project on
-    radius : float
-            the radius of the sphere to project on
-
-    returns: array of floats of shape (npoints, ndim)
-            the points projected onto the sphere
-    """
-
-    lengths = scipy.spatial.distance.cdist(points, np.array([center]))
-    return (points - center) / lengths * radius + center
-
 
 class SphericalVoronoi:
     """ Voronoi diagrams on the surface of a sphere.
@@ -114,10 +57,8 @@ class SphericalVoronoi:
         the points in 3D to generate the Voronoi diagram from
     radius : double
         radius of the sphere
-        Default: None (forces estimation, which is less precise)
     center : double array of shape (3,)
         center of the sphere
-        Default: None (assumes sphere is centered at origin)
     vertices : double array of shape (nvertices, 3)
         Voronoi vertices corresponding to points
     regions : list of list of integers of shape (npoints, _ )
@@ -135,35 +76,19 @@ class SphericalVoronoi:
     The spherical Voronoi diagram algorithm proceeds as follows. The Convex
     Hull of the input points (generators) is calculated, and is equivalent to
     their Delaunay triangulation on the surface of the sphere [Caroli]_.
-    A 3D Delaunay tetrahedralization is obtained by including the origin of
-    the coordinate system as the fourth vertex of each simplex of the Convex
-    Hull. The circumcenters of all tetrahedra in the system are calculated and
-    projected to the surface of the sphere, producing the Voronoi vertices.
-    The Delaunay tetrahedralization neighbour information is then used to
+    The Convex Hull neighbour information is then used to
     order the Voronoi region vertices around each generator. The latter
     approach is substantially less sensitive to floating point issues than
     angle-based methods of Voronoi region vertex sorting.
 
-    The surface area of spherical polygons is calculated by decomposing them
-    into triangles and using L'Huilier's Theorem to calculate the spherical
-    excess of each triangle [Weisstein]_. The sum of the spherical excesses is
-    multiplied by the square of the sphere radius to obtain the surface area
-    of the spherical polygon. For nearly-degenerate spherical polygons an area
-    of approximately 0 is returned by default, rather than attempting the
-    unstable calculation.
-
     Empirical assessment of spherical Voronoi algorithm performance suggests
     quadratic time complexity (loglinear is optimal, but algorithms are more
-    challenging to implement). The reconstitution of the surface area of the
-    sphere, measured as the sum of the surface areas of all Voronoi regions,
-    is closest to 100 % for larger (>> 10) numbers of generators.
+    challenging to implement).
 
     References
     ----------
     .. [Caroli] Caroli et al. Robust and Efficient Delaunay triangulations of
                 points on or close to a sphere. Research Report RR-7004, 2009.
-    .. [Weisstein] "L'Huilier's Theorem." From MathWorld -- A Wolfram Web
-                Resource. http://mathworld.wolfram.com/LHuiliersTheorem.html
 
     See Also
     --------
@@ -216,16 +141,26 @@ class SphericalVoronoi:
 
     """
 
-    def __init__(self, points, radius=None, center=None, threshold=1e-06):
+    def __init__(self, points, radius=1, center=(0, 0, 0), threshold=1e-06):
+        if radius is None:
+            radius = 1.
+            warnings.warn('`radius` is `None`. '
+                          'This will raise an error in a future version. '
+                          'Please provide a floating point number '
+                          '(i.e. `radius=1`).',
+                          DeprecationWarning)
+
+        if center is None:
+            center = (0, 0, 0)
+            warnings.warn('`center` is `None`. '
+                          'This will raise an error in a future version. '
+                          'Please provide a coordinate '
+                          '(i.e. `center=(0, 0, 0)`)',
+                          DeprecationWarning)
+
         self.points = points
-        if np.any(center):
-            self.center = center
-        else:
-            self.center = np.zeros(3)
-        if radius:
-            self.radius = radius
-        else:
-            self.radius = 1
+        self.radius = radius
+        self.center = np.array(center)
 
         if cKDTree(self.points).query_pairs(threshold * self.radius):
             raise ValueError("Duplicate generators present.")
@@ -250,32 +185,15 @@ def _calc_vertices_regions(self):
         Tyler Reddy, Ross Hemsley and Nikolai Nowaczyk
         """
 
-        # perform 3D Delaunay triangulation on data set
-        # (here ConvexHull can also be used, and is faster)
+        # get Convex Hull
         self._tri = scipy.spatial.ConvexHull(self.points)
 
-        # add the center to each of the simplices in tri to get the same
-        # tetrahedrons we'd have gotten from Delaunay tetrahedralization
-        # tetrahedrons will have shape: (2N-4, 4, 3)
-        tetrahedrons = self._tri.points[self._tri.simplices]
-        tetrahedrons = np.insert(
-            tetrahedrons,
-            3,
-            np.array([self.center]),
-            axis=1
-        )
-
-        # produce circumcenters of tetrahedrons from 3D Delaunay
-        # circumcenters will have shape: (2N-4, 3)
-        circumcenters = calc_circumcenters(tetrahedrons)
+        # triangles will have shape: (2N-4, 3, 3)
+        triangles = self._tri.points[self._tri.simplices]
 
-        # project tetrahedron circumcenters to the surface of the sphere
-        # self.vertices will have shape: (2N-4, 3)
-        self.vertices = project_to_sphere(
-            circumcenters,
-            self.center,
-            self.radius
-        )
+        # get circumcenters of Convex Hull triangles from facet equations
+        # circumcenters will have shape: (2N-4, 3)
+        self.vertices = self.radius * self._tri.equations[:, :-1] + self.center
 
         # calculate regions from triangulation
         # simplex_indices will have shape: (2N-4,)
@@ -308,14 +226,13 @@ def sort_vertices_of_regions(self):
 
         This is done as follows: Recall that the n-th region in regions
         surrounds the n-th generator in points and that the k-th
-        Voronoi vertex in vertices is the projected circumcenter of the
-        tetrahedron obtained by the k-th triangle in _tri.simplices (and the
-        origin). For each region n, we choose the first triangle (=Voronoi
-        vertex) in _tri.simplices and a vertex of that triangle not equal to
-        the center n. These determine a unique neighbor of that triangle,
-        which is then chosen as the second triangle. The second triangle
-        will have a unique vertex not equal to the current vertex or the
-        center. This determines a unique neighbor of the second triangle,
+        Voronoi vertex in vertices is the circumcenter of the k-th triangle
+        in _tri.simplices.  For each region n, we choose the first triangle
+        (=Voronoi vertex) in _tri.simplices and a vertex of that triangle
+        not equal to the center n. These determine a unique neighbor of that
+        triangle, which is then chosen as the second triangle. The second
+        triangle will have a unique vertex not equal to the current vertex or
+        the center. This determines a unique neighbor of the second triangle,
         which is then chosen as the third triangle and so forth. We proceed
         through all the triangles (=Voronoi vertices) belonging to the
         generator in points and obtain a sorted version of the vertices


Test patch:
diff --git a/scipy/spatial/tests/test_spherical_voronoi.py b/scipy/spatial/tests/test_spherical_voronoi.py
--- a/scipy/spatial/tests/test_spherical_voronoi.py
+++ b/scipy/spatial/tests/test_spherical_voronoi.py
@@ -5,60 +5,12 @@
                            assert_almost_equal,
                            assert_array_equal,
                            assert_array_almost_equal)
+import warnings
 from pytest import raises as assert_raises
+from pytest import warns as assert_warns
 from scipy.spatial import SphericalVoronoi, distance
 from scipy.spatial import _spherical_voronoi as spherical_voronoi
-
-
-class TestCircumcenters(object):
-
-    def test_circumcenters(self):
-        tetrahedrons = np.array([
-            [[1, 2, 3],
-             [-1.1, -2.1, -3.1],
-             [-1.2, 2.2, 3.2],
-             [-1.3, -2.3, 3.3]],
-            [[10, 20, 30],
-             [-10.1, -20.1, -30.1],
-             [-10.2, 20.2, 30.2],
-             [-10.3, -20.3, 30.3]]
-        ])
-
-        result = spherical_voronoi.calc_circumcenters(tetrahedrons)
-
-        expected = [
-            [-0.5680861153262529, -0.133279590288315, 0.1843323216995444],
-            [-0.5965330784014926, -0.1480377040397778, 0.1981967854886021]
-        ]
-
-        assert_array_almost_equal(result, expected)
-
-
-class TestProjectToSphere(object):
-
-    def test_unit_sphere(self):
-        points = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
-        center = np.array([0, 0, 0])
-        radius = 1
-        projected = spherical_voronoi.project_to_sphere(points, center, radius)
-        assert_array_almost_equal(points, projected)
-
-    def test_scaled_points(self):
-        points = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
-        center = np.array([0, 0, 0])
-        radius = 1
-        scaled = points * 2
-        projected = spherical_voronoi.project_to_sphere(scaled, center, radius)
-        assert_array_almost_equal(points, projected)
-
-    def test_translated_sphere(self):
-        points = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
-        center = np.array([1, 2, 3])
-        translated = points + center
-        radius = 1
-        projected = spherical_voronoi.project_to_sphere(translated, center,
-                                                        radius)
-        assert_array_almost_equal(translated, projected)
+from scipy._lib._numpy_compat import suppress_warnings
 
 
 class TestSphericalVoronoi(object):
@@ -75,6 +27,27 @@ def setup_method(self):
             [0.79979205, 0.54555747, 0.25039913]]
         )
 
+        # Issue #9386
+        self.hemisphere_points = np.array([
+            [0.88610999, -0.42383021, 0.18755541],
+            [0.51980039, -0.72622668, 0.4498915],
+            [0.56540011, -0.81629197, -0.11827989],
+            [0.69659682, -0.69972598, 0.15854467]])
+
+        # Issue #8859
+        phi = np.linspace(0, 2 * np.pi, 10, endpoint=False)    # azimuth angle
+        theta = np.linspace(0.001, np.pi * 0.4, 5)    # polar angle
+        theta = theta[np.newaxis, :].T
+
+        phiv, thetav = np.meshgrid(phi, theta)
+        phiv = np.reshape(phiv, (50, 1))
+        thetav = np.reshape(thetav, (50, 1))
+
+        x = np.cos(phiv) * np.sin(thetav)
+        y = np.sin(phiv) * np.sin(thetav)
+        z = np.cos(thetav)
+        self.hemisphere_points2 = np.concatenate([x, y, z], axis=1)
+
     def test_constructor(self):
         center = np.array([1, 2, 3])
         radius = 2
@@ -83,7 +56,7 @@ def test_constructor(self):
         # the radius / center to match the generators so adjust
         # accordingly here
         s2 = SphericalVoronoi(self.points * radius, radius)
-        s3 = SphericalVoronoi(self.points + center, None, center)
+        s3 = SphericalVoronoi(self.points + center, center=center)
         s4 = SphericalVoronoi(self.points * radius + center, radius, center)
         assert_array_equal(s1.center, np.array([0, 0, 0]))
         assert_equal(s1.radius, 1)
@@ -97,7 +70,7 @@ def test_constructor(self):
     def test_vertices_regions_translation_invariance(self):
         sv_origin = SphericalVoronoi(self.points)
         center = np.array([1, 1, 1])
-        sv_translated = SphericalVoronoi(self.points + center, None, center)
+        sv_translated = SphericalVoronoi(self.points + center, center=center)
         assert_array_equal(sv_origin.regions, sv_translated.regions)
         assert_array_almost_equal(sv_origin.vertices + center,
                                   sv_translated.vertices)
@@ -109,6 +82,29 @@ def test_vertices_regions_scaling_invariance(self):
         assert_array_almost_equal(sv_unit.vertices * 2,
                                   sv_scaled.vertices)
 
+    def test_old_radius_api(self):
+        sv_unit = SphericalVoronoi(self.points, radius=1)
+        with suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "`radius` is `None`")
+            sv = SphericalVoronoi(self.points, None)
+            assert_array_almost_equal(sv_unit.vertices, sv.vertices)
+
+    def test_old_radius_api_warning(self):
+        with assert_warns(DeprecationWarning):
+            sv = SphericalVoronoi(self.points, None)
+
+    def test_old_center_api(self):
+        sv_unit = SphericalVoronoi(self.points, radius=1, center=(0, 0, 0))
+        with suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "`radius` is `None`")
+            sup.filter(DeprecationWarning, "`center` is `None`")
+            sv = SphericalVoronoi(self.points, None, None)
+            assert_array_almost_equal(sv_unit.vertices, sv.vertices)
+
+    def test_old_center_api_warning(self):
+        with assert_warns(DeprecationWarning):
+            sv = SphericalVoronoi(self.points, None, None)
+
     def test_sort_vertices_of_regions(self):
         sv = SphericalVoronoi(self.points)
         unsorted_regions = sv.regions
@@ -164,3 +160,13 @@ def test_incorrect_center_handling(self):
         with assert_raises(ValueError):
             sv = spherical_voronoi.SphericalVoronoi(self.points,
                                                     center=[0.1,0,0])
+
+    def test_single_hemisphere_handling(self):
+        # Test solution of Issues #9386, #8859
+
+        for points in [self.hemisphere_points, self.hemisphere_points2]:
+            sv = SphericalVoronoi(points)
+            triangles = sv._tri.points[sv._tri.simplices]
+            dots = np.einsum('ij,ij->i', sv.vertices, triangles[:, 0])
+            circumradii = np.arccos(np.clip(dots, -1, 1))
+            assert np.max(circumradii) > np.pi / 2