Exemplo n.º 1
0
    def setUp(self):

        self.maxDiff = None

        # trivial example, simple square lattice for testing
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H"], [[0, 0, 0]])
        self.square_sg = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
        self.square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
        # TODO: decorating still fails because the structure graph gives a CN of 8 for this square lattice
        # self.square_sg.decorate_structure_with_ce_info()

        # body-centered square lattice for testing
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])
        self.bc_square_sg = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
        self.bc_square_sg.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, 0, 0))
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(-1, -1, 0))
        self.bc_square_sg.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))

        # body-centered square lattice for testing
        # directions reversed, should be equivalent to bc_square
        structure = Structure(Lattice.tetragonal(5.0, 50.0), ["H", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])
        self.bc_square_sg_r = StructureGraph.with_empty_graph(structure, edge_weight_name="", edge_weight_units="")
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(1, 0, 0))
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(-1, 0, 0))
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, 1, 0))
        self.bc_square_sg_r.add_edge(0, 0, from_jimage=(0, 0, 0), to_jimage=(0, -1, 0))
        self.bc_square_sg_r.add_edge(0, 1, from_jimage=(0, 0, 0), to_jimage=(0, 0, 0))
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(-1, 0, 0), to_jimage=(0, 0, 0))
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(-1, -1, 0), to_jimage=(0, 0, 0))
        self.bc_square_sg_r.add_edge(1, 0, from_jimage=(0, -1, 0), to_jimage=(0, 0, 0))

        # MoS2 example, structure graph obtained from critic2
        # (not ground state, from mp-1023924, single layer)
        stdout_file = os.path.join(PymatgenTest.TEST_FILES_DIR, "critic2/MoS2_critic2_stdout.txt")
        with open(stdout_file) as f:
            reference_stdout = f.read()
        self.structure = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "critic2/MoS2.cif"))
        c2o = Critic2Analysis(self.structure, reference_stdout)
        self.mos2_sg = c2o.structure_graph(include_critical_points=False)

        latt = Lattice.cubic(4.17)
        species = ["Ni", "O"]
        coords = [[0, 0, 0], [0.5, 0.5, 0.5]]
        self.NiO = Structure.from_spacegroup(225, latt, species, coords).get_primitive_structure()

        # BCC example.
        self.bcc = Structure(Lattice.cubic(5.0), ["He", "He"], [[0, 0, 0], [0.5, 0.5, 0.5]])

        warnings.simplefilter("ignore")
Exemplo n.º 2
0
    def _preprocess_input_to_graph(
        input: Union[Structure, StructureGraph, Molecule, MoleculeGraph],
        bonding_strategy: str = "CrystalNN",
        bonding_strategy_kwargs: Optional[Dict] = None,
    ) -> Union[StructureGraph, MoleculeGraph]:

        if isinstance(input, Structure):

            # ensure fractional co-ordinates are normalized to be in [0,1)
            # (this is actually not guaranteed by Structure)
            input = input.as_dict(verbosity=0)
            for site in input["sites"]:
                site["abc"] = np.mod(site["abc"], 1)
            input = Structure.from_dict(input)

            if not input.is_ordered:
                # calculating bonds in disordered structures is currently very flaky
                bonding_strategy = "CutOffDictNN"

        # we assume most uses of this class will give a structure as an input argument,
        # meaning we have to calculate the graph for bonding information, however if
        # the graph is already known and supplied, we will use that
        if isinstance(input, StructureGraph) or isinstance(
                input, MoleculeGraph):
            graph = input
        else:
            if (bonding_strategy not in StructureMoleculeComponent.
                    available_bonding_strategies.keys()):
                raise ValueError(
                    "Bonding strategy not supported. Please supply a name "
                    "of a NearNeighbor subclass, choose from: {}".format(
                        ", ".join(StructureMoleculeComponent.
                                  available_bonding_strategies.keys())))
            else:
                bonding_strategy_kwargs = bonding_strategy_kwargs or {}
                if bonding_strategy == "CutOffDictNN":
                    if "cut_off_dict" in bonding_strategy_kwargs:
                        # TODO: remove this hack by making args properly JSON serializable
                        bonding_strategy_kwargs["cut_off_dict"] = {
                            (x[0], x[1]): x[2]
                            for x in bonding_strategy_kwargs["cut_off_dict"]
                        }
                bonding_strategy = StructureMoleculeComponent.available_bonding_strategies[
                    bonding_strategy](**bonding_strategy_kwargs)
                try:
                    if isinstance(input, Structure):
                        graph = StructureGraph.with_local_env_strategy(
                            input, bonding_strategy)
                    else:
                        graph = MoleculeGraph.with_local_env_strategy(
                            input, bonding_strategy)
                except:
                    # for some reason computing bonds failed, so let's not have any bonds(!)
                    if isinstance(input, Structure):
                        graph = StructureGraph.with_empty_graph(input)
                    else:
                        graph = MoleculeGraph.with_empty_graph(input)

        return graph
Exemplo n.º 3
0
    def structure_graph(self, edge_weight="bond_length", edge_weight_units="Å"):
        """
        A StructureGraph object describing bonding information
        in the crystal. Lazily constructed.

        :param edge_weight: a value to store on the Graph edges,
        by default this is "bond_length" but other supported
        values are any of the attributes of CriticalPoint
        :return:
        """

        sg = StructureGraph.with_empty_graph(self.structure, name="bonds",
                                             edge_weight_name=edge_weight,
                                             edge_weight_units=edge_weight_units)

        edges = self.edges.copy()
        idx_to_delete = []
        # check for duplicate bonds
        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only check edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:
                if idx not in idx_to_delete:
                    for idx2, edge2 in edges.items():
                        if idx != idx2 and edge == edge2:
                            idx_to_delete.append(idx2)
                            warnings.warn("Duplicate edge detected, try re-running "
                                          "critic2 with custom parameters to fix this. "
                                          "Mostly harmless unless user is also "
                                          "interested in rings/cages.")
                            logger.debug("Duplicate edge between points {} (unique point {})"
                                         "and {} ({}).".format(idx, self.nodes[idx]['unique_idx'],
                                                               idx2, self.nodes[idx2]['unique_idx']))
        # and remove any duplicate bonds present
        for idx in idx_to_delete:
            del edges[idx]

        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only add edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:

                from_idx = edge['from_idx']
                to_idx = edge['to_idx']

                from_lvec = edge['from_lvec']
                to_lvec = edge['to_lvec']

                if edge_weight == "bond_length":
                    weight = self.structure.get_distance(from_idx, to_idx)
                else:
                    weight = getattr(self.critical_points[unique_idx],
                                     edge_weight, None)

                sg.add_edge(from_idx, to_idx,
                            from_jimage=from_lvec, to_jimage=to_lvec,
                            weight=weight)

        return sg
Exemplo n.º 4
0
    def get_interaction_graph(self, filename=None):
        """
        Get a StructureGraph with edges and weights that correspond to exchange
        interactions and J_ij values, respectively.

        Args:
            filename (str): if not None, save interaction graph to filename.
        Returns:
            igraph (StructureGraph): Exchange interaction graph.
        """

        structure = self.ordered_structures[0]
        sgraph = self.sgraphs[0]
        unique_site_ids = self.unique_site_ids
        dists = self.dists
        tol = self.tol

        igraph = StructureGraph.with_empty_graph(
            structure,
            edge_weight_name="exchange_constant",
            edge_weight_units="meV")

        if "<J>" in self.ex_params:  # Only <J> is available
            warning_msg = """
                Only <J> is available. The interaction graph will not tell
                you much.
                """
            warnings.warn(warning_msg)

        # J_ij exchange interaction matrix
        for i, node in enumerate(sgraph.graph.nodes):
            connections = sgraph.get_connected_sites(i)
            for c in connections:
                jimage = c[1]  # relative integer coordinates of atom j
                dx = jimage[0]
                dy = jimage[1]
                dz = jimage[2]
                j = c[2]  # index of neighbor
                dist = c[-1]  # i <-> j distance

                j_exc = self._get_j_exc(i, j, dist)

                igraph.add_edge(i,
                                j,
                                to_jimage=jimage,
                                weight=j_exc,
                                warn_duplicates=False)

        # Save to a json file if desired
        if filename:
            if filename.endswith(".json"):
                dumpfn(igraph, filename)
            else:
                filename += ".json"
                dumpfn(igraph, filename)

        return igraph
Exemplo n.º 5
0
def make_scene_dicts(parchg_list, defect_pos, level=None):
    band_indices = [c.split(".")[-2] for c in parchg_list]
    # parchgs = [Chgcar.from_file(c) for c in parchg_list]
    parchgs = []
    for c in parchg_list:
        try:
            parchgs.append(Chgcar.from_file(c))
        except ValueError:
            continue
    if len(parchgs) == 0:
        return None
    structure: Structure = parchgs[0].structure
    if len(parchg_list) > 1:
        for parchg in parchgs[1:]:
            assert structure == parchg.structure

    lattice_matrix = parchgs[0].structure.lattice.matrix

    # centering to [0.5, 0.5, 0.5]
    shift_vector = np.array([0.5 - pos for pos in defect_pos])
    shape = parchgs[0].data["total"].shape
    shift_numbers = np.array(np.rint(shape * shift_vector), dtype=np.int)
    actual_shift_vector = shift_numbers / shape

    step_size = int(min(parchgs[0].dim) / 30)
    results = {}
    for name, parchg in zip(band_indices, parchgs):
        for spin in [Spin.up, Spin.down]:
            spin_str = "up" if spin is Spin.up else "down"
            key = name + "_" + spin_str
            data = parchg.spin_data[spin]
            x_shifted = np.roll(data, shift_numbers[0], axis=0)
            xy_shifted = np.roll(x_shifted, shift_numbers[1], axis=1)
            xyz_shifted = np.roll(xy_shifted, shift_numbers[2], axis=2)
            try:
                vertices, faces = get_vertices_and_faces(xyz_shifted,
                                                         lattice_matrix,
                                                         step_size=step_size,
                                                         level=level)
                results[key] = {"vertices": vertices, "faces": faces}
            except RuntimeError:
                continue

    structure.translate_sites(indices=list(range(len(structure))),
                              vector=actual_shift_vector)

    graph = StructureGraph.with_empty_graph(structure=structure)
    return SceneDicts(results, structure_graph=graph)
Exemplo n.º 6
0
    def _preprocess_input_to_graph(
        input: Union[Structure, StructureGraph],
        bonding_strategy: str = DEFAULTS["bonding_strategy"],
        bonding_strategy_kwargs: Optional[Dict] = None,
    ) -> Union[StructureGraph, MoleculeGraph]:

        # ensure fractional co-ordinates are normalized to be in [0,1)
        # (this is actually not guaranteed by Structure)
        try:
            input = input.as_dict(verbosity=0)
        except TypeError:
            # TODO: remove this, necessary for Slab(?), some structure subclasses don't have verbosity
            input = input.as_dict()
        for site in input["sites"]:
            site["abc"] = np.mod(site["abc"], 1)
        input = Structure.from_dict(input)

        if not input.is_ordered:
            # calculating bonds in disordered structures is currently very flaky
            bonding_strategy = "CutOffDictNN"

        if (bonding_strategy
                not in StructureComponent.available_bonding_strategies.keys()):
            raise ValueError(
                "Bonding strategy not supported. Please supply a name "
                "of a NearNeighbor subclass, choose from: {}".format(", ".join(
                    StructureComponent.available_bonding_strategies.keys())))
        else:
            bonding_strategy_kwargs = bonding_strategy_kwargs or {}
            if bonding_strategy == "CutOffDictNN":
                if "cut_off_dict" in bonding_strategy_kwargs:
                    # TODO: remove this hack by making args properly JSON serializable
                    bonding_strategy_kwargs["cut_off_dict"] = {
                        (x[0], x[1]): x[2]
                        for x in bonding_strategy_kwargs["cut_off_dict"]
                    }
            bonding_strategy = StructureComponent.available_bonding_strategies[
                bonding_strategy](**bonding_strategy_kwargs)
            try:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore")
                    graph = StructureGraph.with_local_env_strategy(
                        input, bonding_strategy)
            except:
                # for some reason computing bonds failed, so let's not have any bonds(!)
                graph = StructureGraph.with_empty_graph(input)

        return graph
Exemplo n.º 7
0
    def __init__(self, supercell_info: SupercellInfo, *args, **kwargs):
        s = Structure.from_dict(supercell_info.structure.as_dict())
        coords = [[f % 1 for f in x.frac_coords] for x in s]
        structure = Structure(lattice=s.lattice,
                              species=s.species,
                              coords=coords)
        self.graph = StructureGraph.with_empty_graph(structure=structure)

        self.transformation_matrix = supercell_info.transformation_matrix
        self.interstitials = supercell_info.interstitials
        super().__init__(*args, **kwargs)

        self.title = "supercell size: " + supercell_info._transform_matrix_str

        scene, legend = self.get_scene_and_legend()
        self.create_store("legend_data", initial_data=legend)
        self.create_store("graph", initial_data=self.graph)

        # this is used by a Simple3DScene component, not a dcc.Store
        self._initial_data["scene"] = scene
Exemplo n.º 8
0
    def structure_graph(self,
                        edge_weight="bond_length",
                        edge_weight_units="Å"):
        """
        A StructureGraph object describing bonding information
        in the crystal. Lazily constructed.

        :param edge_weight: a value to store on the Graph edges,
        by default this is "bond_length" but other supported
        values are any of the attributes of CriticalPoint
        :return:
        """

        sg = StructureGraph.with_empty_graph(
            self.structure,
            name="bonds",
            edge_weight_name=edge_weight,
            edge_weight_units=edge_weight_units)

        edges = self.edges.copy()
        idx_to_delete = []
        # check for duplicate bonds
        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only check edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:
                if idx not in idx_to_delete:
                    for idx2, edge2 in edges.items():
                        if idx != idx2 and edge == edge2:
                            idx_to_delete.append(idx2)
                            warnings.warn(
                                "Duplicate edge detected, try re-running "
                                "critic2 with custom parameters to fix this. "
                                "Mostly harmless unless user is also "
                                "interested in rings/cages.")
                            logger.debug(
                                "Duplicate edge between points {} (unique point {})"
                                "and {} ({}).".format(
                                    idx, self.nodes[idx]['unique_idx'], idx2,
                                    self.nodes[idx2]['unique_idx']))
        # and remove any duplicate bonds present
        for idx in idx_to_delete:
            del edges[idx]

        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only add edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:

                from_idx = edge['from_idx']
                to_idx = edge['to_idx']

                from_lvec = edge['from_lvec']
                to_lvec = edge['to_lvec']

                relative_lvec = np.subtract(to_lvec, from_lvec)

                if edge_weight == "bond_length":
                    weight = self.structure.get_distance(from_idx,
                                                         to_idx,
                                                         jimage=relative_lvec)
                else:
                    weight = getattr(self.critical_points[unique_idx],
                                     edge_weight, None)

                sg.add_edge(from_idx,
                            to_idx,
                            from_jimage=from_lvec,
                            to_jimage=to_lvec,
                            weight=weight)

        return sg
Exemplo n.º 9
0
    def structure_graph(self,
                        include_critical_points=("bond", "ring", "cage")):
        """
        A StructureGraph object describing bonding information
        in the crystal.
        Args:
            include_critical_points: add DummySpecie for
            the critical points themselves, a list of
            "nucleus", "bond", "ring", "cage", set to None
            to disable

        Returns: a StructureGraph
        """

        structure = self.structure.copy()

        point_idx_to_struct_idx = {}
        if include_critical_points:
            # atoms themselves don't have field information
            # so set to 0
            for prop in ("ellipticity", "laplacian", "field"):
                structure.add_site_property(prop, [0] * len(structure))
            for idx, node in self.nodes.items():
                cp = self.critical_points[node["unique_idx"]]
                if cp.type.value in include_critical_points:
                    specie = DummySpecie("X{}cp".format(cp.type.value[0]),
                                         oxidation_state=None)
                    structure.append(
                        specie,
                        node["frac_coords"],
                        properties={
                            "ellipticity": cp.ellipticity,
                            "laplacian": cp.laplacian,
                            "field": cp.field,
                        },
                    )
                    point_idx_to_struct_idx[idx] = len(structure) - 1

        edge_weight = "bond_length"
        edge_weight_units = "Å"

        sg = StructureGraph.with_empty_graph(
            structure,
            name="bonds",
            edge_weight_name=edge_weight,
            edge_weight_units=edge_weight_units,
        )

        edges = self.edges.copy()
        idx_to_delete = []
        # check for duplicate bonds
        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]["unique_idx"]
            # only check edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:
                if idx not in idx_to_delete:
                    for idx2, edge2 in edges.items():
                        if idx != idx2 and edge == edge2:
                            idx_to_delete.append(idx2)
                            warnings.warn(
                                "Duplicate edge detected, try re-running "
                                "critic2 with custom parameters to fix this. "
                                "Mostly harmless unless user is also "
                                "interested in rings/cages.")
                            logger.debug(
                                "Duplicate edge between points {} (unique point {})"
                                "and {} ({}).".format(
                                    idx,
                                    self.nodes[idx]["unique_idx"],
                                    idx2,
                                    self.nodes[idx2]["unique_idx"],
                                ))
        # and remove any duplicate bonds present
        for idx in idx_to_delete:
            del edges[idx]

        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]["unique_idx"]
            # only add edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:

                from_idx = edge["from_idx"]
                to_idx = edge["to_idx"]

                # have to also check bond is between nuclei if non-nuclear
                # attractors not in structure
                skip_bond = False
                if include_critical_points and "nnattr" not in include_critical_points:
                    from_type = self.critical_points[self.nodes[from_idx]
                                                     ["unique_idx"]].type
                    to_type = self.critical_points[self.nodes[from_idx]
                                                   ["unique_idx"]].type
                    skip_bond = (from_type != CriticalPointType.nucleus) or (
                        to_type != CriticalPointType.nucleus)

                if not skip_bond:
                    from_lvec = edge["from_lvec"]
                    to_lvec = edge["to_lvec"]

                    relative_lvec = np.subtract(to_lvec, from_lvec)

                    # for edge case of including nnattrs in bonding graph when other critical
                    # points also included, indices may get mixed
                    struct_from_idx = point_idx_to_struct_idx.get(
                        from_idx, from_idx)
                    struct_to_idx = point_idx_to_struct_idx.get(to_idx, to_idx)

                    weight = self.structure.get_distance(struct_from_idx,
                                                         struct_to_idx,
                                                         jimage=relative_lvec)

                    crit_point = self.critical_points[unique_idx]

                    edge_properties = {
                        "field": crit_point.field,
                        "laplacian": crit_point.laplacian,
                        "ellipticity": crit_point.ellipticity,
                        "frac_coords": self.nodes[idx]["frac_coords"],
                    }

                    sg.add_edge(
                        struct_from_idx,
                        struct_to_idx,
                        from_jimage=from_lvec,
                        to_jimage=to_lvec,
                        weight=weight,
                        edge_properties=edge_properties,
                    )

        return sg
Exemplo n.º 10
0
    def test_auto_image_detection(self):

        sg = StructureGraph.with_empty_graph(self.structure)
        sg.add_edge(0, 0)

        self.assertEqual(len(list(sg.graph.edges(data=True))), 3)
Exemplo n.º 11
0
    def structure_graph(self,
                        edge_weight=None,
                        edge_weight_units=None,
                        include_critical_points=("bond", "ring", "cage")):
        """
        A StructureGraph object describing bonding information
        in the crystal. Lazily constructed.
        Args:
            edge_weight: a value to store on the Graph edges,
            by default this is "bond_length" but other supported
            values are any of the attributes of CriticalPoint
            edge_weight_units: optional metadata for
            book-keeping (e.g. Å for "bond_length" edge weight)
            include_critical_points: add DummySpecie for
            the critical points themselves, a list of
            "nucleus", "bond", "ring", "cage", set to None
            to disable

        Returns: a StructureGraph
        """

        structure = self.structure.copy()

        if include_critical_points:
            # atoms themselves don't have field information
            # so set to 0
            for prop in ("ellipticity", "laplacian", "field"):
                structure.add_site_property(prop, [0] * len(structure))
            for idx, node in self.nodes.items():
                cp = self.critical_points[node["unique_idx"]]
                if cp.type.value in include_critical_points:
                    specie = DummySpecie("{}cp".format(cp.type.value[0]),
                                         oxidation_state=None)
                    structure.append(specie,
                                     node["frac_coords"],
                                     properties={
                                         "ellipticity": cp.ellipticity,
                                         "laplacian": cp.laplacian,
                                         "field": cp.field
                                     })

        sg = StructureGraph.with_empty_graph(
            structure,
            name="bonds",
            edge_weight_name=edge_weight,
            edge_weight_units=edge_weight_units)

        edges = self.edges.copy()
        idx_to_delete = []
        # check for duplicate bonds
        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only check edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:
                if idx not in idx_to_delete:
                    for idx2, edge2 in edges.items():
                        if idx != idx2 and edge == edge2:
                            idx_to_delete.append(idx2)
                            warnings.warn(
                                "Duplicate edge detected, try re-running "
                                "critic2 with custom parameters to fix this. "
                                "Mostly harmless unless user is also "
                                "interested in rings/cages.")
                            logger.debug(
                                "Duplicate edge between points {} (unique point {})"
                                "and {} ({}).".format(
                                    idx, self.nodes[idx]['unique_idx'], idx2,
                                    self.nodes[idx2]['unique_idx']))
        # and remove any duplicate bonds present
        for idx in idx_to_delete:
            del edges[idx]

        for idx, edge in edges.items():
            unique_idx = self.nodes[idx]['unique_idx']
            # only add edges representing bonds, not rings
            if self.critical_points[unique_idx].type == CriticalPointType.bond:

                from_idx = edge['from_idx']
                to_idx = edge['to_idx']

                from_lvec = edge['from_lvec']
                to_lvec = edge['to_lvec']

                relative_lvec = np.subtract(to_lvec, from_lvec)

                if edge_weight == "bond_length":
                    weight = self.structure.get_distance(from_idx,
                                                         to_idx,
                                                         jimage=relative_lvec)
                elif edge_weight:
                    weight = getattr(self.critical_points[unique_idx],
                                     edge_weight, None)
                else:
                    weight = None

                sg.add_edge(from_idx,
                            to_idx,
                            from_jimage=from_lvec,
                            to_jimage=to_lvec,
                            weight=weight)

        return sg