# Delaunay graph example

An example illustrating graph manipulation and display with Mayavi and NetworkX.

This example shows how to use Mayavi in a purely algorithmic way, to compute a Delaunay from data points, extract it and pass it to networkx. It also shows how to plot a graph using quiver.

Starting from points positioned regularly on a sphere, we first use VTK to create the Delaunay graph, and also to plot it. We then create a matching NetworkX graph, calling it’s minimum spanning tree function. We display it using Mayavi.

The visualization clearly shows that the minimum spanning tree of the points, considering all possible connections, is included in the Delaunay graph.

The function compute_delaunay_edges uses VTK to retrieve the Delaunay graph of a set of points. First a structure of unconnected points is created using mlab.points3d. The Delaunay filter applied to it builds an unstructured grid (see Data representation in Mayavi). We apply an ExtractEdges filter to it, which returns a structure of points connected by edges: the PolyData structure. The dataset structure can be retrieved as the first item of the outputs list of the ExtractEdges filter object, returned by the mlab.pipeline.extract_edges factory function. Once we have this object, we extract the points and edge list from it. This graph-plotting technique differs from the technique used in the examples Protein example and Flight graph example where points are created and connected by lines. Unlike these techniques, it enables storing scalar data on each line.

To visualize the graph (function graph_plot), we build a list of vectors giving the edges, and use mlab.quiver3d to display them. To display an unoriented graph, it is best to use the 2ddash mode of quiver3d.

Python source code: `delaunay_graph.py`

    # Author: Gary Ruben
    #         Gael Varoquaux <gael dot varoquaux at normalesup dot org>
    # Copyright (c) 2009, Enthought, Inc.
    # License: BSD style.

    from mayavi import mlab
    import numpy as np
    import networkx as nx

    def compute_delaunay_edges(x, y, z, visualize=False):
        """ Given 3-D points, returns the edges of their
            Delaunay triangulation.

            Parameters
            -----------
            x: ndarray
                x coordinates of the points
            y: ndarray
                y coordinates of the points
            z: ndarray
                z coordinates of the points

            Returns
            ---------
            new_x: ndarray
                new x coordinates of the points (same coords but different
                assignment of points)
            new_y: ndarray
                new y coordinates of the points (same coords but different
                assignment of points)
            new_z: ndarray
                new z coordinates of the points (same coords but different
                assignment of points)
            edges: 2D ndarray.
                The indices of the edges of the Delaunay triangulation as a
                (N, 2) array [[pair1_index1, pair1_index2],
                              [pair2_index1, pair2_index2],
                              ...                         ]
        """
        if visualize:
            vtk_source = mlab.points3d(x, y, z, opacity=0.3, mode='2dvertex')
            vtk_source.actor.property.point_size = 3
        else:
            vtk_source =  mlab.pipeline.scalar_scatter(x, y, z, figure=False)
        delaunay =  mlab.pipeline.delaunay3d(vtk_source)
        delaunay.filter.offset = 999    # seems more reliable than the default
        edges = mlab.pipeline.extract_edges(delaunay)
        if visualize:
            mlab.pipeline.surface(edges, opacity=0.3, line_width=3)

        # We extract the output array. the 'points' attribute itself
        # is a TVTK array, that we convert to a numpy array using
        # its 'to_array' method.
        new_x, new_y, new_z = edges.outputs[0].points.to_array().T
        lines = edges.outputs[0].lines.to_array()
        return new_x, new_y, new_z, np.array([lines[1::3], lines[2::3]]).T

    def graph_plot(x, y, z, start_idx, end_idx, edge_scalars=None, **kwargs):
        """ Show the graph edges using Mayavi

            Parameters
            -----------
            x: ndarray
                x coordinates of the points
            y: ndarray
                y coordinates of the points
            z: ndarray
                z coordinates of the points
            edge_scalars: ndarray, optional
                optional data to give the color of the edges.
            kwargs:
                extra keyword arguments are passed to quiver3d.
        """
        vec = mlab.quiver3d(x[start_idx],
                            y[start_idx],
                            z[start_idx],
                            x[end_idx] - x[start_idx],
                            y[end_idx] - y[start_idx],
                            z[end_idx] - z[start_idx],
                            scalars=edge_scalars,
                            mode='2ddash',
                            scale_factor=1,
                            **kwargs)
        if edge_scalars is not None:
            vec.glyph.color_mode = 'color_by_scalar'
        return vec

    def build_geometric_graph(x, y, z, edges):
        """ Build a NetworkX graph with xyz node coordinates and the node indices
            of the end nodes.

            Parameters
            -----------
            x: ndarray
                x coordinates of the points
            y: ndarray
                y coordinates of the points
            z: ndarray
                z coordinates of the points
            edges: the (2, N) array returned by compute_delaunay_edges()
                containing node indices of the end nodes. Weights are applied to
                the edges based on their euclidean length for use by the MST
                algorithm.

            Returns
            ---------
            g: A NetworkX undirected graph

            Notes
            ------
            We don't bother putting the coordinates into the NX graph.
            Instead the graph node is an index to the column.
        """
        xyz = np.array((x, y, z))
        def euclidean_dist(i, j):
            d = xyz[:,i] - xyz[:,j]
            return np.sqrt(np.dot(d, d))

        g = nx.Graph()
        for i, j in edges:
            if nx.__version__.split('.')[0] > '0':
                g.add_edge(i, j, weight=euclidean_dist(i, j))
            else:
                g.add_edge(i, j, euclidean_dist(i, j))
        return g

    def points_on_sphere(N):
        """ Generate N evenly distributed points on the unit sphere centered at
            the origin. Uses the 'Golden Spiral'.
            Code by Chris Colbert from the numpy-discussion list.
        """
        phi = (1 + np.sqrt(5)) / 2  # the golden ratio
        long_incr = 2*np.pi / phi   # how much to increment the longitude

        dz = 2.0 / float(N)         # a unit sphere has diameter 2
        bands = np.arange(N)        # each band will have one point placed on it
        z = bands * dz - 1 + (dz/2) # the height z of each band/point
        r = np.sqrt(1 - z*z)        # project onto xy-plane
        az = bands * long_incr      # azimuthal angle of point modulo 2 pi
        x = r * np.cos(az)
        y = r * np.sin(az)
        return x, y, z

    ################################################################################
    if __name__ == '__main__':
        # generate some points
        x, y, z = points_on_sphere(50)
        # Avoid triangulation problems on the sphere
        z *= 1.01

        mlab.figure(1, bgcolor=(0,0,0))
        mlab.clf()

        # Now get the Delaunay Triangulation from vtk via mayavi mlab. Vtk stores
        # its points in a different order so overwrite ours to match the edges
        new_x, new_y, new_z, edges = compute_delaunay_edges(x, y, z, visualize=True)
        assert(x.shape == new_x.shape)   # check triangulation got everything
        x, y, z = new_x, new_y, new_z

        if nx.__version__ < '0.99':
            raise ImportError('The version of NetworkX must be at least '
                        '0.99 to run this example')

        # Make a NetworkX graph out of our point and edge data
        g = build_geometric_graph(x, y, z, edges)

        # Compute minimum spanning tree using networkx
        # nx.mst returns an edge generator
        edges = nx.minimum_spanning_tree(g).edges(data=True)
        start_idx, end_idx, _ = np.array(list(edges)).T
        start_idx = start_idx.astype(np.int)
        end_idx   = end_idx.astype(np.int)

        # Plot this with Mayavi
        graph_plot(x, y, z, start_idx, end_idx,
                    edge_scalars=z[start_idx],
                    opacity=0.8,
                    colormap='summer',
                    line_width=4,
                    )

        mlab.view(60, 46, 4)
        mlab.show()