Repository: scipy/scipy

Issue:
Using Qhull for halfspace intersection : segfault
As the title says, Qhull support computing halfspace intersections via its `H` option or the dedicated `qhalf`program.

One should give a list of halfspaces of the form:

> Ax + b <= 0

And an interior (feasible) point. Then, Qhull will compute all the vertices forming this polyhedron. Unfortunately, this is bugged for now, even for [a simple example](https://gist.github.com/haudren/8546d3866480aadeaa757dac14f810f2)

I will investigate a bit, but the program crashes when calling Qhull.get_simplex_facet_array(), meaning that in `H` mode, some assumptions are not valid.

At a quick glance, I'd say this is related to the fact that get_simplex_facet_array() uses `self.ndim` and `self.numpoints` but those are not the dimensions of the input array in `H` mode.



Discussion:
Ok, I started working on this on my [fork](https://github.com/haudren/scipy/tree/topic/qhullHalfspaces). It appears that correctly setting `ndim` and `num_points` solves the issue. Indeed, the above test now passes.

Unfortunately, one more point needs to be dealt with. Whenever working with halfspaces, Qhull works in the dual space of the original problem. Meaning that:
- Neither points nor halfspaces correspond to the input
- The real intersection points have to be computed separately (see `qhull/io.c:1239`) using the simple formula:

``` python
points = hull.equations[:, :-1]/hull.equations[:,:-1]+feasible_point
```

My question is thus, what is the best way to introduce this ? I think that we should not touch the `points` array as it should stay the same as the input, but it seems to me that it would be much nicer to be able to retrieve the above data easily.

Moreover, for now I serialize by hand the feasible point:

``` python
qhull_options='H{}'.format(','.join(map(str, feasible_point.squeeze().tolist())))
```

It would be preferable to pass it as an extra keyword argument.

Note: a simple example for a square:

``` python
Original hyperplanes:
[[-1.  0.  0.]
 [ 0. -1.  0.]
 [ 1.  0. -1.]
 [ 0.  1. -1.]]
Feasible point:
[[0.5, 0.5]]
Contents of hull.points:
[[-1.  0.  0.]
 [ 0. -1.  0.]
 [ 1.  0. -1.]
 [ 0.  1. -1.]]
Contents of vertex.points (Directly extracted from underlying _qh object):
[[ 0.  2.]
 [ 0. -2.]
 [-2.  0.]
 [ 2.  0.]]
Contents of equations:
[[-0.70710678  0.70710678 -1.41421356]
 [ 0.70710678  0.70710678 -1.41421356]
 [-0.70710678 -0.70710678 -1.41421356]
 [ 0.70710678 -0.70710678 -1.41421356]]
Computed halfspace intersections:
[[ 1.  0.]
 [ 0.  0.]
 [ 1.  1.]
 [ 0.  1.]]
```

The half-space computation probably should be considered a separate 
"major mode" similar to Delaunay. (That ConvexHull accepts the H flags 
at all is probably a bug.)
.
You can either add a new method to the _QHull class for pulling out the 
half-space information, or adjust one of the existing ones to suit this, 
if only small changes are needed. E.g. via adding a new keyword argument 
`half_space=False` that toggles between modes.
.
The `H` flags should not be passed via qhull_options, since it's a major 
mode. I haven't used this mode, but perhaps the qhull API has a way to 
pass in the points in an array rather than having to go via strings.

After tinkering with it a bit, I think a major mode will be necessary: in halfspace mode, Qhull does not compute at all the triangulation of the intersections, and I am afraid we will encounter _a lot_ of confusion by putting different things in the same container.

I'll look into it and try to submit a patch sometime this week!

For the second option, it seems that it is possible when using a file (you can specify the interior point first, then specify the halfspaces), so I'll check which method they call to set it.


Resolving patch:
diff --git a/scipy/spatial/__init__.py b/scipy/spatial/__init__.py
--- a/scipy/spatial/__init__.py
+++ b/scipy/spatial/__init__.py
@@ -25,6 +25,7 @@
    ConvexHull  -- compute a convex hull for input points
    Voronoi     -- compute a Voronoi diagram hull from input points
    SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere
+   HalfspaceIntersection -- compute the intersection points of input halfspaces
 
 Plotting Helpers
 ================


Test patch:
diff --git a/scipy/spatial/tests/data/selfdual-4d-polytope.txt b/scipy/spatial/tests/data/selfdual-4d-polytope.txt
new file mode 100644
--- /dev/null
+++ b/scipy/spatial/tests/data/selfdual-4d-polytope.txt
@@ -0,0 +1,27 @@
+# The facets of a self-dual 4-dim regular polytope
+# with 24 octahedron facets. Taken from cddlib.
+# Format b + Ax >= 0
+ 1  1  1  1  1
+ 1  1  1  1 -1
+ 1  1  1 -1  1
+ 1  1  1 -1 -1
+ 1  1 -1  1  1
+ 1  1 -1  1 -1
+ 1  1 -1 -1  1
+ 1  1 -1 -1 -1
+ 1 -1  1  1  1
+ 1 -1  1  1 -1
+ 1 -1  1 -1  1
+ 1 -1  1 -1 -1
+ 1 -1 -1  1  1
+ 1 -1 -1  1 -1
+ 1 -1 -1 -1  1
+ 1 -1 -1 -1 -1
+ 1  2  0  0  0
+ 1  0  2  0  0
+ 1  0  0  2  0
+ 1  0  0  0  2
+ 1 -2  0  0  0
+ 1  0 -2  0  0
+ 1  0  0 -2  0
+ 1  0  0  0 -2
diff --git a/scipy/spatial/tests/test_qhull.py b/scipy/spatial/tests/test_qhull.py
--- a/scipy/spatial/tests/test_qhull.py
+++ b/scipy/spatial/tests/test_qhull.py
@@ -12,6 +12,7 @@
 import scipy.spatial.qhull as qhull
 from scipy.spatial import cKDTree as KDTree
 
+import itertools
 
 def sorted_tuple(x):
     return tuple(sorted(x))
@@ -902,6 +903,114 @@ def simplified(x):
 
             yield check, name
 
+class Test_HalfspaceIntersection(object):
+    def assert_unordered_allclose(self, arr1, arr2, rtol=1e-7):
+        """Check that every line in arr1 is only once in arr2"""
+        assert_equal(arr1.shape, arr2.shape)
+
+        truths = np.zeros((arr1.shape[0],), dtype=bool)
+        for l1 in arr1:
+            indexes = np.where((abs(arr2 - l1) < rtol).all(axis=1))[0]
+            assert_equal(indexes.shape, (1,))
+            truths[indexes[0]] = True
+        assert_(truths.all())
+
+    def test_cube_halfspace_intersection(self):
+        halfspaces = np.array([[-1.0, 0.0, 0.0],
+                               [0.0, -1.0, 0.0],
+                               [1.0, 0.0, -1.0],
+                               [0.0, 1.0, -1.0]])
+        feasible_point = np.array([0.5, 0.5])
+
+        points = np.array([[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0]])
+
+        hull = qhull.HalfspaceIntersection(halfspaces, feasible_point)
+
+        assert_allclose(points, hull.intersections)
+
+    def test_self_dual_polytope_intersection(self):
+        fname = os.path.join(os.path.dirname(__file__), 'data',
+                             'selfdual-4d-polytope.txt')
+        ineqs = np.genfromtxt(fname)
+        halfspaces = -np.hstack((ineqs[:, 1:], ineqs[:, :1]))
+
+        feas_point = np.array([0., 0., 0., 0.])
+        hs = qhull.HalfspaceIntersection(halfspaces, feas_point)
+
+        assert_equal(hs.intersections.shape, (24, 4))
+
+        assert_almost_equal(hs.dual_volume, 32.0)
+        assert_equal(len(hs.dual_facets), 24)
+        for facet in hs.dual_facets:
+            assert_equal(len(facet), 6)
+
+        dists = halfspaces[:, -1] + halfspaces[:, :-1].dot(feas_point)
+        self.assert_unordered_allclose((halfspaces[:, :-1].T/dists).T, hs.dual_points)
+
+        points = itertools.permutations([0., 0., 0.5, -0.5])
+        for point in points:
+            assert_equal(np.sum((hs.intersections == point).all(axis=1)), 1)
+
+    def test_wrong_feasible_point(self):
+        halfspaces = np.array([[-1.0, 0.0, 0.0],
+                               [0.0, -1.0, 0.0],
+                               [1.0, 0.0, -1.0],
+                               [0.0, 1.0, -1.0]])
+        feasible_point = np.array([0.5, 0.5, 0.5])
+        #Feasible point is (ndim,) instead of (ndim-1,)
+        assert_raises(ValueError, qhull.HalfspaceIntersection, halfspaces, feasible_point)
+        feasible_point = np.array([[0.5], [0.5]])
+        #Feasible point is (ndim-1, 1) instead of (ndim-1,)
+        assert_raises(ValueError, qhull.HalfspaceIntersection, halfspaces, feasible_point)
+        feasible_point = np.array([[0.5, 0.5]])
+        #Feasible point is (1, ndim-1) instead of (ndim-1,)
+        assert_raises(ValueError, qhull.HalfspaceIntersection, halfspaces, feasible_point)
+
+        feasible_point = np.array([-0.5, -0.5])
+        #Feasible point is outside feasible region
+        assert_raises(qhull.QhullError, qhull.HalfspaceIntersection, halfspaces, feasible_point)
+
+    def test_incremental(self):
+        #Cube
+        halfspaces = np.array([[0., 0., -1., -0.5],
+                               [0., -1., 0., -0.5],
+                               [-1., 0., 0., -0.5],
+                               [1., 0., 0., -0.5],
+                               [0., 1., 0., -0.5],
+                               [0., 0., 1., -0.5]])
+        #Cut each summit
+        extra_normals = np.array([[1., 1., 1.],
+                                  [1., 1., -1.],
+                                  [1., -1., 1.],
+                                  [1, -1., -1.]])
+        offsets = np.array([[-1.]]*8)
+        extra_halfspaces = np.hstack((np.vstack((extra_normals, -extra_normals)),
+                                      offsets))
+
+        feas_point = np.array([0., 0., 0.])
+
+        inc_hs = qhull.HalfspaceIntersection(halfspaces, feas_point, incremental=True)
+
+        inc_res_hs = qhull.HalfspaceIntersection(halfspaces, feas_point, incremental=True)
+
+        for i, ehs in enumerate(extra_halfspaces):
+            inc_hs.add_halfspaces(ehs[np.newaxis, :])
+
+            inc_res_hs.add_halfspaces(ehs[np.newaxis, :], restart=True)
+
+            total = np.vstack((halfspaces, extra_halfspaces[:i+1, :]))
+
+            hs = qhull.HalfspaceIntersection(total, feas_point)
+
+            assert_allclose(inc_hs.halfspaces, inc_res_hs.halfspaces)
+            assert_allclose(inc_hs.halfspaces, hs.halfspaces)
+
+            #Direct computation and restart should have points in same order
+            assert_allclose(hs.intersections, inc_res_hs.intersections)
+            #Incremental will have points in different order than direct computation
+            self.assert_unordered_allclose(inc_hs.intersections, hs.intersections)
+
+        inc_hs.close()
 
 if __name__ == "__main__":
     run_module_suite()