Add edge to graph.

        Since physically a 'bond' (or other connection
        between sites) doesn't have a direction, from_index,
        from_jimage can be swapped with to_index, to_jimage.

        However, images will always always be shifted so that
        from_index < to_index and from_jimage becomes (0, 0, 0).

        :param from_index: index of site connecting from
        :param to_index: index of site connecting to
        :param from_jimage (tuple of ints): lattice vector of periodic
        image, e.g. (1, 0, 0) for periodic image in +x direction
        :param to_jimage (tuple of ints): lattice vector of image
        :param weight (float): e.g. bond length
        :param warn_duplicates (bool): if True, will warn if
        trying to add duplicate edges (duplicate edges will not
        be added in either case)
        :param edge_properties (dict): any other information to
        store on graph edges, similar to Structure's site_properties
        :return:

def add_edge(self, from_index, to_index,
                 from_jimage=(0, 0, 0), to_jimage=None,
                 weight=None, warn_duplicates=True,
                 edge_properties=None):
        """
        Add edge to graph.

        Since physically a 'bond' (or other connection
        between sites) doesn't have a direction, from_index,
        from_jimage can be swapped with to_index, to_jimage.

        However, images will always always be shifted so that
        from_index < to_index and from_jimage becomes (0, 0, 0).

        :param from_index: index of site connecting from
        :param to_index: index of site connecting to
        :param from_jimage (tuple of ints): lattice vector of periodic
        image, e.g. (1, 0, 0) for periodic image in +x direction
        :param to_jimage (tuple of ints): lattice vector of image
        :param weight (float): e.g. bond length
        :param warn_duplicates (bool): if True, will warn if
        trying to add duplicate edges (duplicate edges will not
        be added in either case)
        :param edge_properties (dict): any other information to
        store on graph edges, similar to Structure's site_properties
        :return:
        """

        # this is not necessary for the class to work, but
        # just makes it neater
        if to_index < from_index:
            to_index, from_index = from_index, to_index
            to_jimage, from_jimage = from_jimage, to_jimage

        # constrain all from_jimages to be (0, 0, 0),
        # initial version of this class worked even if
        # from_jimage != (0, 0, 0), but making this
        # assumption simplifies logic later
        if not np.array_equal(from_jimage, (0, 0, 0)):
            shift = from_jimage
            from_jimage = np.subtract(from_jimage, shift)
            to_jimage = np.subtract(to_jimage, shift)

        # automatic detection of to_jimage if user doesn't specify
        # will try and detect all equivalent images and add multiple
        # edges if appropriate
        if to_jimage is None:
            # assume we want the closest site
            warnings.warn("Please specify to_jimage to be unambiguous, "
                          "trying to automatically detect.")
            dist, to_jimage = self.structure[from_index]\
                .distance_and_image(self.structure[to_index])
            if dist == 0:
                # this will happen when from_index == to_index,
                # typically in primitive single-atom lattices
                images = [1, 0, 0], [0, 1, 0], [0, 0, 1]
                dists = []
                for image in images:
                    dists.append(self.structure[from_index]
                                 .distance_and_image(self.structure[from_index],
                                                                               jimage=image)[0])
                dist = min(dists)
            equiv_sites = self.structure.get_neighbors_in_shell(self.structure[from_index].coords,
                                                                dist,
                                                                dist*0.01,
                                                                include_index=True)
            for site, dist, to_index in equiv_sites:
                to_jimage = np.subtract(site.frac_coords, self.structure[from_index].frac_coords)
                to_jimage = to_jimage.astype(int)
                self.add_edge(from_index=from_index, from_jimage=(0, 0, 0),
                              to_jimage=to_jimage, to_index=to_index)
            return

        # sanitize types
        from_jimage, to_jimage = tuple(map(int, from_jimage)), tuple(map(int, to_jimage))
        from_index, to_index = int(from_index), int(to_index)

        # check we're not trying to add a duplicate edge
        # there should only ever be at most one edge
        # between a given (site, jimage) pair and another
        # (site, jimage) pair
        existing_edge_data = self.graph.get_edge_data(from_index, to_index)
        if existing_edge_data:
            for key, d in existing_edge_data.items():
                if d["to_jimage"] == to_jimage:
                    if warn_duplicates:
                        warnings.warn("Trying to add an edge that already exists from "
                                      "site {} to site {} in {}.".format(from_index,
                                                                         to_index,
                                                                         to_jimage))
                    return

        # generic container for additional edge properties,
        # similar to site properties
        edge_properties = edge_properties or {}

        if weight:
            self.graph.add_edge(from_index, to_index,
                                to_jimage=to_jimage,
                                weight=weight,
                                **edge_properties)
        else:
            self.graph.add_edge(from_index, to_index,
                                to_jimage=to_jimage,
                                **edge_properties)