示例#1
0
class JmolNNTest(PymatgenTest):

    def setUp(self):
        self.jmol = JmolNN()
        self.jmol_update = JmolNN(el_radius_updates={"Li": 1})

    def test_get_nn(self):
        s = self.get_structure('LiFePO4')

        # Test the default near-neighbor finder.
        nsites_checked = 0

        for site_idx, site in enumerate(s):
            if site.specie == Element("Li"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 0)
                nsites_checked += 1
            elif site.specie == Element("Fe"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 6)
                nsites_checked += 1
            elif site.specie == Element("P"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 4)
                nsites_checked += 1
        self.assertEqual(nsites_checked, 12)

        # Test a user override that would cause Li to show up as 6-coordinated
        self.assertEqual(self.jmol_update.get_cn(s, 0), 6)

        # Verify get_nn function works
        self.assertEqual(len(self.jmol_update.get_nn(s, 0)), 6)

    def tearDown(self):
        del self.jmol
        del self.jmol_update
示例#2
0
class JmolNNTest(PymatgenTest):

    def setUp(self):
        self.jmol = JmolNN()
        self.jmol_update = JmolNN(el_radius_updates={"Li": 1})

    def test_get_nn(self):
        s = self.get_structure('LiFePO4')

        # Test the default near-neighbor finder.
        nsites_checked = 0

        for site_idx, site in enumerate(s):
            if site.specie == Element("Li"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 0)
                nsites_checked += 1
            elif site.specie == Element("Fe"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 6)
                nsites_checked += 1
            elif site.specie == Element("P"):
                self.assertEqual(self.jmol.get_cn(s, site_idx), 4)
                nsites_checked += 1
        self.assertEqual(nsites_checked, 12)

        # Test a user override that would cause Li to show up as 6-coordinated
        self.assertEqual(self.jmol_update.get_cn(s, 0), 6)

        # Verify get_nn function works
        self.assertEqual(len(self.jmol_update.get_nn(s, 0)), 6)

    def tearDown(self):
        del self.jmol
        del self.jmol_update
示例#3
0
 def _set_cnn(self, method="JmolNN", porous_adjustment: bool = True):
     self.porous_adjustment = porous_adjustment
     if self._cnn is None or self._cnn_method != method.lower():
         if method.lower() == "crystalnn":
             self._cnn = CrystalNN(porous_adjustment=self.porous_adjustment)
         else:
             self._cnn = JmolNN()
         self._cnn_method = method.lower()
示例#4
0
def get_local_env_method(method):  # pylint:disable=too-many-return-statements
    """get a local environment method based on its name"""
    method = method.lower()

    if method.lower() == "crystalnn":
        # see eq. 15 and 16 in
        # https://pubs.acs.org/doi/full/10.1021/acs.inorgchem.0c02996
        # for the x_diff_weight parameter.
        # in the paper it is called δen and it is set to 3
        # we found better results by lowering this weight
        return CrystalNN(porous_adjustment=True,
                         x_diff_weight=1.5,
                         search_cutoff=4.5)
    if method.lower() == "econnn":
        return EconNN()
    if method.lower() == "brunnernn":
        return BrunnerNN_relative()
    if method.lower() == "minimumdistance":
        return MinimumDistanceNN()
    if method.lower() == "vesta":
        return VESTA_NN
    if method.lower() == "voronoinn":
        return VoronoiNN()
    if method.lower() == "atr":
        return ATR_NN
    if method.lower() == "li":
        return LI_NN

    return JmolNN()
示例#5
0
    def compute_graph(self, index, cached=False, method="jmolnn"):
        if cached:
            s = self.reduced_structure_dict[
                self.scalar_feature_matrix.iloc[index]["name"]]
        else:
            s = Structure.from_file(
                self.scalar_feature_matrix.iloc[index]["name"])

        if method == "jmolnn":
            nn_strategy = JmolNN()
        elif method == "crystalgraph":
            nn_strategy = CrystalNN()
        else:
            nn_strategy = JmolNN()

        graph = StructureGraph.with_local_env_strategy(s, nn_strategy)

        return graph
示例#6
0
def find_dimension(structure_raw, tolerance=0.45, ldict=JmolNN().el_radius, standardize=True):
    """
    Algorithm for finding the dimensions of connected subunits in a crystal structure.
    This method finds the dimensionality of the material even when the material is not layered along low-index planes,
    or does not have flat layers/molecular wires.
    See details at : Cheon, G.; Duerloo, K.-A. N.; Sendek, A. D.; Porter, C.; Chen, Y.; Reed, E. J. Data Mining for
    New Two- and One-Dimensional Weakly Bonded Solids and Lattice-Commensurate Heterostructures. Nano Lett. 2017.

    Args:
        structure (Structure): Input structure
        tolerance: length in angstroms used in finding bonded atoms. Two atoms are considered bonded if (radius of
            atom 1) + (radius of atom 2) + (tolerance) < (distance between atoms 1 and 2). Default value = 0.45, the
            value used by JMol and Cheon et al.
        ldict: dictionary of bond lengths used in finding bonded atoms. Values from JMol are used as default
        standardize: works with conventional standard structures if True. It is recommended to keep this as True.

    Returns:
        dim: dimension of the largest cluster as a string. If there are ions or molecules it returns 'intercalated
        ion/molecule'
    """
    if standardize:
        structure = SpacegroupAnalyzer(structure_raw).get_conventional_standard_structure()
    structure_save = copy.copy(structure_raw)
    connected_list1 = find_connected_atoms(structure, tolerance=tolerance, ldict=ldict)
    max1, min1, clusters1 = find_clusters(structure, connected_list1)
    structure.make_supercell([[2, 0, 0], [0, 2, 0], [0, 0, 2]])
    connected_list2 = find_connected_atoms(structure, tolerance=tolerance, ldict=ldict)
    max2, min2, clusters2 = find_clusters(structure, connected_list2)
    if min2 == 1:
        dim = 'intercalated ion'
    elif min2 == min1:
        if max2 == max1:
            dim = '0D'
        else:
            dim = 'intercalated molecule'
    else:
        dim = np.log2(float(max2) / max1)
        if dim == int(dim):
            dim = str(int(dim)) + 'D'
        else:
            structure = copy.copy(structure_save)
            structure.make_supercell([[3, 0, 0], [0, 3, 0], [0, 0, 3]])
            connected_list3 = find_connected_atoms(structure, tolerance=tolerance, ldict=ldict)
            max3, min3, clusters3 = find_clusters(structure, connected_list3)
            if min3 == min2:
                if max3 == max2:
                    dim = '0D'
                else:
                    dim = 'intercalated molecule'
            else:
                dim = np.log2(float(max3) / max1) / np.log2(3)
                if dim == int(dim):
                    dim = str(int(dim)) + 'D'
                else:
                    return
    return dim
def find_surface_atoms_indices(bulk_cn_dict, atoms):
    struct = AseAtomsAdaptor.get_structure(atoms, cls=None)
    nn_analyzer = JmolNN()

    # Identify index of the surface atoms
    indices_list = []
    weights = [site.species.weight for site in struct]
    center_of_mass = np.average(struct.frac_coords, weights=weights, axis=0)

    for idx, site in enumerate(struct):
        if site.frac_coords[2] > center_of_mass[2]:
            try:
                cn = nn_analyzer.get_cn(struct, idx, use_weights=True)
                cn = float('%.5f' % (round(cn, 5)))
                # surface atoms are undercoordinated
                if cn < min(bulk_cn_dict[site.species_string]):
                    indices_list.append(idx)
            except RuntimeError:
                # or if pathological error is returned,
                # indicating a surface site
                indices_list.append(idx)
    return indices_list
示例#8
0
 def test_cns(self):
     cnv = CoordinationNumber.from_preset('VoronoiNN')
     self.assertEqual(len(cnv.feature_labels()), 1)
     self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN')
     self.assertAlmostEqual(cnv.featurize(self.sc, 0)[0], 6)
     self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 14)
     self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 14)
     self.assertEqual(len(cnv.citations()), 2)
     cnv = CoordinationNumber(VoronoiNN(), use_weights='sum')
     self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN')
     self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 9.2584516)
     self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 9.2584516)
     self.assertEqual(len(cnv.citations()), 2)
     cnv = CoordinationNumber(VoronoiNN(), use_weights='effective')
     self.assertEqual(cnv.feature_labels()[0], 'CN_VoronoiNN')
     self.assertAlmostEqual(cnv.featurize(self.cscl, 0)[0], 11.648923254)
     self.assertAlmostEqual(cnv.featurize(self.cscl, 1)[0], 11.648923254)
     self.assertEqual(len(cnv.citations()), 2)
     cnj = CoordinationNumber.from_preset('JmolNN')
     self.assertEqual(cnj.feature_labels()[0], 'CN_JmolNN')
     self.assertAlmostEqual(cnj.featurize(self.sc, 0)[0], 0)
     self.assertAlmostEqual(cnj.featurize(self.cscl, 0)[0], 0)
     self.assertAlmostEqual(cnj.featurize(self.cscl, 1)[0], 0)
     self.assertEqual(len(cnj.citations()), 1)
     jmnn = JmolNN(el_radius_updates={"Al": 1.55, "Cl": 1.7, "Cs": 1.7})
     cnj = CoordinationNumber(jmnn)
     self.assertEqual(cnj.feature_labels()[0], 'CN_JmolNN')
     self.assertAlmostEqual(cnj.featurize(self.sc, 0)[0], 6)
     self.assertAlmostEqual(cnj.featurize(self.cscl, 0)[0], 8)
     self.assertAlmostEqual(cnj.featurize(self.cscl, 1)[0], 8)
     self.assertEqual(len(cnj.citations()), 1)
     cnmd = CoordinationNumber.from_preset('MinimumDistanceNN')
     self.assertEqual(cnmd.feature_labels()[0], 'CN_MinimumDistanceNN')
     self.assertAlmostEqual(cnmd.featurize(self.sc, 0)[0], 6)
     self.assertAlmostEqual(cnmd.featurize(self.cscl, 0)[0], 8)
     self.assertAlmostEqual(cnmd.featurize(self.cscl, 1)[0], 8)
     self.assertEqual(len(cnmd.citations()), 1)
     cnmok = CoordinationNumber.from_preset('MinimumOKeeffeNN')
     self.assertEqual(cnmok.feature_labels()[0], 'CN_MinimumOKeeffeNN')
     self.assertAlmostEqual(cnmok.featurize(self.sc, 0)[0], 6)
     self.assertAlmostEqual(cnmok.featurize(self.cscl, 0)[0], 8)
     self.assertAlmostEqual(cnmok.featurize(self.cscl, 1)[0], 6)
     self.assertEqual(len(cnmok.citations()), 2)
     cnmvire = CoordinationNumber.from_preset('MinimumVIRENN')
     self.assertEqual(cnmvire.feature_labels()[0], 'CN_MinimumVIRENN')
     self.assertAlmostEqual(cnmvire.featurize(self.sc, 0)[0], 6)
     self.assertAlmostEqual(cnmvire.featurize(self.cscl, 0)[0], 8)
     self.assertAlmostEqual(cnmvire.featurize(self.cscl, 1)[0], 14)
     self.assertEqual(len(cnmvire.citations()), 2)
     self.assertEqual(len(cnmvire.implementors()), 2)
     self.assertEqual(cnmvire.implementors()[0], 'Nils E. R. Zimmermann')
示例#9
0
def get_max_bond_lengths(structure, el_radius_updates=None):
    """
    Provides max bond length estimates for a structure based on the JMol
    table and algorithms.

    Args:
        structure: (structure)
        el_radius_updates: (dict) symbol->float to update atomic radii

    Returns: (dict) - (Element1, Element2) -> float. The two elements are
        ordered by Z.
    """
    # jmc = JMolCoordFinder(el_radius_updates)
    jmnn = JmolNN(el_radius_updates=el_radius_updates)

    bonds_lens = {}
    els = sorted(structure.composition.elements, key=lambda x: x.Z)

    for i1, el1 in enumerate(els):
        for i2 in range(len(els) - i1):
            bonds_lens[el1, els[i1 + i2]] = jmnn.get_max_bond_distance(el1.symbol, els[i1 + i2].symbol)

    return bonds_lens
示例#10
0
def find_connected_atoms(struct, tolerance=0.45, ldict=JmolNN().el_radius):
    """
    Finds the list of bonded atoms.

    Author: "Gowoon Cheon"
    Email: "*****@*****.**"

    Args:
        struct (Structure): Input structure
        tolerance: length in angstroms used in finding bonded atoms. Two atoms
            are considered bonded if (radius of atom 1) + (radius of atom 2) +
            (tolerance) < (distance between atoms 1 and 2). Default
            value = 0.45, the value used by JMol and Cheon et al.
        ldict: dictionary of bond lengths used in finding bonded atoms. Values
            from JMol are used as default

    Returns:
        (np.ndarray): A numpy array of shape (number of bonded pairs, 2); each
        row of is of the form [atomi, atomj]. atomi and atomj are the indices of
        the atoms in the input structure. If any image of atomj is bonded to
        atomi with periodic boundary conditions, [atomi, atomj] is included in
        the list. If atomi is bonded to multiple images of atomj, it is only
        counted once.
    """
    n_atoms = len(struct.species)
    fc = np.array(struct.frac_coords)
    species = list(map(str, struct.species))

    # in case of charged species
    for i, item in enumerate(species):
        if item not in ldict.keys():
            species[i] = str(Specie.from_string(item).element)
    latmat = struct.lattice.matrix
    connected_list = []

    for i in range(n_atoms):
        for j in range(i + 1, n_atoms):
            max_bond_length = ldict[species[i]] + ldict[species[j]] + tolerance
            add_ij = False
            for move_cell in itertools.product([0, 1, -1], [0, 1, -1],
                                               [0, 1, -1]):
                if not add_ij:
                    frac_diff = fc[j] + move_cell - fc[i]
                    distance_ij = np.dot(latmat.T, frac_diff)
                    if np.linalg.norm(distance_ij) < max_bond_length:
                        add_ij = True
            if add_ij:
                connected_list.append([i, j])
    return np.array(connected_list)
示例#11
0
def get_max_bond_lengths(structure, el_radius_updates=None):
    """
    Provides max bond length estimates for a structure based on the JMol
    table and algorithms.
    
    Args:
        structure: (structure)
        el_radius_updates: (dict) symbol->float to update atomic radii
    
    Returns: (dict) - (Element1, Element2) -> float. The two elements are 
        ordered by Z.
    """
    #jmc = JMolCoordFinder(el_radius_updates)
    jmnn = JmolNN(el_radius_updates=el_radius_updates)

    bonds_lens = {}
    els = sorted(structure.composition.elements, key=lambda x: x.Z)

    for i1 in range(len(els)):
        for i2 in range(len(els) - i1):
            bonds_lens[els[i1], els[i1 + i2]] = jmnn.get_max_bond_distance(
                els[i1].symbol, els[i1 + i2].symbol)

    return bonds_lens
示例#12
0
def find_connected_atoms(struct, tolerance=0.45, ldict=JmolNN().el_radius):
    """
    Finds bonded atoms and returns a adjacency matrix of bonded atoms.

    Author: "Gowoon Cheon"
    Email: "*****@*****.**"

    Args:
        struct (Structure): Input structure
        tolerance: length in angstroms used in finding bonded atoms. Two atoms
            are considered bonded if (radius of atom 1) + (radius of atom 2) +
            (tolerance) < (distance between atoms 1 and 2). Default
            value = 0.45, the value used by JMol and Cheon et al.
        ldict: dictionary of bond lengths used in finding bonded atoms. Values
            from JMol are used as default

    Returns:
        (np.ndarray): A numpy array of shape (number of atoms, number of atoms);
        If any image of atom j is bonded to atom i with periodic boundary
        conditions, the matrix element [atom i, atom j] is 1.
    """
    # pylint: disable=E1136
    n_atoms = len(struct.species)
    fc = np.array(struct.frac_coords)
    fc_copy = np.repeat(fc[:, :, np.newaxis], 27, axis=2)
    neighbors = np.array(
        list(itertools.product([0, 1, -1], [0, 1, -1], [0, 1, -1]))).T
    neighbors = np.repeat(neighbors[np.newaxis, :, :], 1, axis=0)
    fc_diff = fc_copy - neighbors
    species = list(map(str, struct.species))
    # in case of charged species
    for i, item in enumerate(species):
        if item not in ldict.keys():
            species[i] = str(Species.from_string(item).element)
    latmat = struct.lattice.matrix
    connected_matrix = np.zeros((n_atoms, n_atoms))

    for i in range(n_atoms):
        for j in range(i + 1, n_atoms):
            max_bond_length = ldict[species[i]] + ldict[species[j]] + tolerance
            frac_diff = fc_diff[j] - fc_copy[i]
            distance_ij = np.dot(latmat.T, frac_diff)
            # print(np.linalg.norm(distance_ij,axis=0))
            if sum(np.linalg.norm(distance_ij, axis=0) < max_bond_length) > 0:
                connected_matrix[i, j] = 1
                connected_matrix[j, i] = 1
    return connected_matrix
示例#13
0
def get_hash(structure: Structure, get_niggli=True):
    """
    This gets hash, using the structure graph as a part of the has

    Args:
        structure: pymatgen structure object
        get_niggli (bool):
    Returns:

    """
    if get_niggli:
        crystal = structure.get_reduced_structure()
    else:
        crystal = structure
    nn_strategy = JmolNN()
    sgraph_a = StructureGraph.with_local_env_strategy(crystal, nn_strategy)
    graph_hash = str(hash(sgraph_a.graph))
    comp_hash = str(hash(str(crystal.symbol_set)))
    density_hash = str(hash(crystal.density))
    return graph_hash + comp_hash + density_hash
 def compare_graph_pair_cached(self, items):
     nn_strategy = JmolNN()
     crystal_a = self.reduced_structure_dict[
         self.scalar_feature_matrix.iloc[items[0]]["name"]]
     crystal_b = self.reduced_structure_dict[
         self.scalar_feature_matrix.iloc[items[1]]["name"]]
     if self.try_supercell:
         crystal_a, crystal_b = attempt_supercell_pymatgen(
             crystal_a, crystal_b)
     sgraph_a = StructureGraph.with_local_env_strategy(
         crystal_a, nn_strategy)
     sgraph_b = StructureGraph.with_local_env_strategy(
         crystal_b, nn_strategy)
     try:
         if sgraph_a == sgraph_b:
             logger.debug("Found duplicate")
             return items
     except ValueError:
         logger.debug("Structures were probably not different")
         return False
示例#15
0
    def check_unbound(s: Structure,
                      whitelist: list = ['H'],
                      threshold: float = 2.5,
                      mode='naive') -> bool:
        """
        This uses the fact that unbound solvent is often in pores
        and more distant from all other atoms. So far this test focusses on water.

        Args:
            s (pymatgen structure object): structure to be checked
            whitelist (list): elements that are not considered in the check (they are basically removed from the
                structure)
            mode (str): checking mode. If 'naive' then a simple distance based check is used and a atom is detected
                as unbound it there is no other atom within the threshold distance.

        Returns:

        """

        crystal = s.copy()

        if whitelist:
            crystal.remove_species(whitelist)

        if mode == 'naive':
            for atom in crystal:
                neighbors = crystal.get_neighbors(atom, threshold)
                if len(neighbors) == 0:
                    return True
            return False

        if mode == 'graph':
            nn_strategy = JmolNN()
            sgraph = StructureGraph.with_local_env_strategy(
                crystal, nn_strategy)
            molecules = get_subgraphs_as_molecules_all(sgraph)

            if len(molecules) > 0:
                return True
            else:
                return False
示例#16
0
    def remove_unbound_solvent(structure: Structure) -> Structure:
        """
        Constructs a structure graph and removes unbound solvent molecules
        if they are in a hardcoded composition list.

        Args:
            structure (pymatgen structure object=:

        Returns:

        """
        crystal = structure.copy()
        molecules_solvent = ['H2 O1', 'H3 O1', 'C2 H6 O S', 'O1']
        nn_strategy = JmolNN()
        sgraph = StructureGraph.with_local_env_strategy(crystal, nn_strategy)
        molecules = get_subgraphs_as_molecules_all(sgraph)
        cart_coordinates = crystal.cart_coords
        indices = []
        for molecule in molecules:
            print(str(molecule.composition))
            if molecule.formula in molecules_solvent:
                for coord in [
                        site.as_dict()['xyz'] for site in molecule.sites
                ]:
                    if (coord[0] < crystal.lattice.a) and (
                            coord[1] < crystal.lattice.b) and (
                                coord[2] < crystal.lattice.c):
                        print(coord)
                        indices.append(
                            np.where(
                                np.prod(
                                    np.isclose(cart_coordinates - coord, 0),
                                    axis=1) == 1)[0][0])

        print(indices)
        crystal.remove_sites(indices)

        return crystal
示例#17
0
def get_dimensionality_cheon(
    structure_raw,
    tolerance=0.45,
    ldict=JmolNN().el_radius,
    standardize=True,
    larger_cell=False,
):
    """
    Algorithm for finding the dimensions of connected subunits in a structure.
    This method finds the dimensionality of the material even when the material
    is not layered along low-index planes, or does not have flat
    layers/molecular wires.

    Author: "Gowoon Cheon"
    Email: "*****@*****.**"

    See details at :

    Cheon, G.; Duerloo, K.-A. N.; Sendek, A. D.; Porter, C.; Chen, Y.; Reed,
    E. J. Data Mining for New Two- and One-Dimensional Weakly Bonded Solids and
    Lattice-Commensurate Heterostructures. Nano Lett. 2017.

    Args:
        structure_raw (Structure): A pymatgen Structure object.
        tolerance (float): length in angstroms used in finding bonded atoms.
            Two atoms are considered bonded if (radius of atom 1) + (radius of
            atom 2) + (tolerance) < (distance between atoms 1 and 2). Default
            value = 0.45, the value used by JMol and Cheon et al.
        ldict (dict): dictionary of bond lengths used in finding bonded atoms.
            Values from JMol are used as default
        standardize: works with conventional standard structures if True. It is
            recommended to keep this as True.
        larger_cell: tests with 3x3x3 supercell instead of 2x2x2. Testing with
            2x2x2 supercell is faster but misclssifies rare interpenetrated 3D
             structures. Testing with a larger cell circumvents this problem

    Returns:
        (str): dimension of the largest cluster as a string. If there are ions
        or molecules it returns 'intercalated ion/molecule'
    """
    if standardize:
        structure = SpacegroupAnalyzer(
            structure_raw).get_conventional_standard_structure()
    else:
        structure = structure_raw
    structure_save = copy.copy(structure_raw)
    connected_list1 = find_connected_atoms(structure,
                                           tolerance=tolerance,
                                           ldict=ldict)
    max1, min1, clusters1 = find_clusters(structure, connected_list1)
    if larger_cell:
        structure.make_supercell([[3, 0, 0], [0, 3, 0], [0, 0, 3]])
        connected_list3 = find_connected_atoms(structure,
                                               tolerance=tolerance,
                                               ldict=ldict)
        max3, min3, clusters3 = find_clusters(structure, connected_list3)
        if min3 == min1:
            if max3 == max1:
                dim = "0D"
            else:
                dim = "intercalated molecule"
        else:
            dim = np.log2(float(max3) / max1) / np.log2(3)
            if dim == int(dim):
                dim = str(int(dim)) + "D"
            else:
                return None
    else:
        structure.make_supercell([[2, 0, 0], [0, 2, 0], [0, 0, 2]])
        connected_list2 = find_connected_atoms(structure,
                                               tolerance=tolerance,
                                               ldict=ldict)
        max2, min2, clusters2 = find_clusters(structure, connected_list2)
        if min2 == 1:
            dim = "intercalated ion"
        elif min2 == min1:
            if max2 == max1:
                dim = "0D"
            else:
                dim = "intercalated molecule"
        else:
            dim = np.log2(float(max2) / max1)
            if dim == int(dim):
                dim = str(int(dim)) + "D"
            else:
                structure = copy.copy(structure_save)
                structure.make_supercell([[3, 0, 0], [0, 3, 0], [0, 0, 3]])
                connected_list3 = find_connected_atoms(structure,
                                                       tolerance=tolerance,
                                                       ldict=ldict)
                max3, min3, clusters3 = find_clusters(structure,
                                                      connected_list3)
                if min3 == min2:
                    if max3 == max2:
                        dim = "0D"
                    else:
                        dim = "intercalated molecule"
                else:
                    dim = np.log2(float(max3) / max1) / np.log2(3)
                    if dim == int(dim):
                        dim = str(int(dim)) + "D"
                    else:
                        return None
    return dim
示例#18
0
 def setUp(self):
     self.jmol = JmolNN()
     self.jmol_update = JmolNN(el_radius_updates={"Li": 1})
示例#19
0
 def update_properties(self):
     self.molgraph = MoleculeGraph.with_local_env_strategy(
         self.mol, JmolNN())
     # self.rings = self.molgraph.find_rings()
     self.rings = self.find_rings()
     self.bonds = self.molgraph.graph.edges(data=False)
示例#20
0
 def get_structure_graph(self, struct: Structure) -> nx.Graph:
     sgraph = StructureGraph.with_local_env_strategy(struct, JmolNN())
     return sgraph
示例#21
0
 def setUp(self):
     self.jmol = JmolNN()
     self.jmol_update = JmolNN(el_radius_updates={"Li": 1})
示例#22
0
class MOFChecker:  # pylint:disable=too-many-instance-attributes, too-many-public-methods
    """MOFChecker performs basic sanity checks for MOFs"""

    def __init__(self, structure: Structure):
        """Class that can perform basic sanity checks for MOF structures

        Args:
            structure (Structure): pymatgen Structure object
        """
        self.structure = structure
        self.metal_indices = [
            i for i, species in enumerate(self.structure.species) if species.is_metal
        ]
        self.porous_adjustment = True
        self.metal_features = None
        self._open_indices: set = set()
        self._has_oms = None
        self._cnn = None
        self._cnn_method = None
        self._filename = None
        self._atomic_overlaps = None
        self._name = None
        self.c_indices = [
            i for i, species in enumerate(self.structure.species) if str(species) == "C"
        ]
        self.h_indices = [
            i for i, species in enumerate(self.structure.species) if str(species) == "H"
        ]
        self.n_indices = [
            i for i, species in enumerate(self.structure.species) if str(species) == "N"
        ]
        self._overvalent_c = None
        self._overvalent_n = None
        self._overvalent_h = None
        self._undercoordinated_carbon = None
        self._undercoordinated_nitrogen = None
        self.check_expected_values = EXPECTED_CHECK_VALUES
        self.check_descriptions = CHECK_DESCRIPTIONS

    def _set_filename(self, path):
        self._filename = os.path.abspath(path)
        self._name = Path(path).stem

    def _get_atomic_overlaps(self):
        if self._atomic_overlaps is not None:
            return self._atomic_overlaps

        self._atomic_overlaps = get_overlaps(self.structure)
        return self._atomic_overlaps

    def get_overlapping_indices(self):
        """Return the indices of overlapping atoms"""
        return self._get_atomic_overlaps()

    @property
    def has_atomic_overlaps(self):
        """Check if there are any overlaps in the structure"""
        atomic_overlaps = self._get_atomic_overlaps()
        return len(atomic_overlaps) > 0

    @property
    def name(self):
        """Return filename if the MOFChecker instance was created based on
        a histogram."""
        return self._name

    @property
    def has_carbon(self):
        """Check if there is any carbon atom in the structure"""
        return len(self.c_indices) > 0

    @property
    def has_hydrogen(self):
        """Check if there is any hydrogen atom in the structure"""
        return len(self.h_indices) > 0

    @property
    def density(self):
        """Density of structure"""
        return self.structure.density

    @property
    def volume(self):
        """Volume of structure in A^3"""
        return self.structure.volume

    @property
    def formula(self):
        """Return the chemical formula of the structure"""
        return self.structure.formula

    @property
    def has_overvalent_c(self) -> bool:
        """Returns true if there is some carbon in the structure
        that has more than 4 neighbors.

        Returns:
            [bool]: True if carbon with CN > 4 in structure.
        """
        if self._overvalent_c is not None:
            return self._overvalent_c

        self._has_overvalent_c()
        return self._overvalent_c

    @property
    def has_overvalent_h(self) -> bool:
        """Returns true if there is some hydrogen in the structure
        that has more than 1 neighbor.

        Returns:
            [bool]: True if hydrogen with CN > 1 in structure.
        """
        if self._overvalent_h is not None:
            return self._overvalent_h

        self._has_overvalent_h()
        return self._overvalent_h

    @property
    def has_undercoordinated_c(self) -> bool:
        """Check if there is a carbon that likely misses
        hydrogen"""
        if self._undercoordinated_carbon is not None:
            return self._undercoordinated_carbon

        self._has_undercoordinated_carbon()
        return self._undercoordinated_carbon

    @property
    def has_undercoordinated_n(self) -> bool:
        """Check if there is a nitrogen that likely misses
        hydrogen"""
        if self._undercoordinated_nitrogen is not None:
            return self._undercoordinated_nitrogen

        self._has_undercoordinated_nitrogen()
        return self._undercoordinated_nitrogen

    def _has_overvalent_c(self):
        overvalent_c = False
        for site_index in self.c_indices:
            cn = self.get_cn(site_index)  # pylint:disable=invalid-name
            if cn > 4:
                overvalent_c = True
                break
        self._overvalent_c = overvalent_c

    def _has_overvalent_h(self):
        overvalent_h = False
        for site_index in self.h_indices:
            cn = self.get_cn(site_index)  # pylint:disable=invalid-name
            if cn > 1:
                overvalent_h = True
                break
        self._overvalent_h = overvalent_h

    def _has_undercoordinated_carbon(self, tolerance=10):
        """Idea is that carbon should at least have three neighbors if it is not sp1.
        In sp1 case it is linear. So we can just check if there are carbons with
        non-linear coordination with less than three neighbors. An example in CoRE
        MOF would be AHOKIR. In principle this should also flag the quite common
        case of benzene rings with missing hydrogens.
        """
        undercoordinated_carbon = False
        for site_index in self.c_indices:
            cn = self.get_cn(site_index)  # pylint:disable=invalid-name
            if cn == 2:
                # ToDo: Check if it is bound to metal, then it might be a carbide
                graph = StructureGraph.with_local_env_strategy(
                    self.structure, self._cnn
                )
                neighbors = graph.get_connected_sites(site_index)
                angle = self.structure.get_angle(
                    site_index, neighbors[0].index, neighbors[1].index
                )
                if np.abs(90 - angle) > tolerance:
                    undercoordinated_carbon = True
                    break
        self._undercoordinated_carbon = undercoordinated_carbon

    def _has_undercoordinated_nitrogen(self, tolerance=10):
        """
        Captures missing hydrogens on amino groups.
        Basically two common cases:
            1. Have the N on a carbon and no hydrogen at all
            2. (not that common) due to incorrect symmetry resolution
                we have only one h in a bent orientation
        """
        undercoordinated_nitrogen = False
        for site_index in self.n_indices:
            cn = self.get_cn(site_index)  # pylint:disable=invalid-name
            if cn == 1:
                # this is suspicous, but it also might a CN wish is perfectly fine
                # to check this, we first see if the neighbor is carbon
                # and then what its coordination number is
                graph = StructureGraph.with_local_env_strategy(
                    self.structure, self._cnn
                )
                neighbors = graph.get_connected_sites(site_index)
                if (self.get_cn(neighbors[0].index) > 2) and (
                    str(neighbors[0].periodic_site.specie) == "C"
                ):
                    undercoordinated_nitrogen = True
                    break
            if cn == 2:
                # ToDo: Check if it is bound to metal, then it might be a nitride
                graph = StructureGraph.with_local_env_strategy(
                    self.structure, self._cnn
                )
                neighbors = graph.get_connected_sites(site_index)
                angle = self.structure.get_angle(
                    site_index, neighbors[0].index, neighbors[1].index
                )
                if np.abs(90 - angle) > tolerance:
                    undercoordinated_nitrogen = True
                    break
        self._undercoordinated_nitrogen = undercoordinated_nitrogen

    @property
    def has_overvalent_n(self) -> bool:
        """Returns true if there is some nitrogen in the structure
        that has more than 4 neighbors.

        Returns:
            [bool]: True if nitrogen with CN > 4 in structure.
        """
        if self._overvalent_n is not None:
            return self._overvalent_n

        self._has_overvalent_n()
        return self._overvalent_n

    @property
    def has_lone_atom(self) -> bool:
        """Returns True if there is a isolated floating atom"""
        return self._has_lone_atom()

    @property
    def has_lone_molecule(self) -> bool:
        """Returns true if there is a isolated floating atom or molecule"""
        return self._has_stray_molecules()

    def _has_lone_atom(self):
        self._set_cnn()
        graph = StructureGraph.with_local_env_strategy(self.structure, self._cnn)
        for site in range(len(self.structure)):
            nbr = graph.get_connected_sites(site)
            if not nbr:
                return True
        return False

    def _has_overvalent_n(self):
        overvalent_n = False
        for site_index in self.n_indices:
            cn = self.get_cn(site_index)  # pylint:disable=invalid-name
            if cn > 4:
                overvalent_n = True
                break
        self._overvalent_n = overvalent_n

    @classmethod
    def _from_file(cls, path: str):
        structure = Structure.from_file(path)
        omscls = cls(structure)
        omscls._set_filename(path)  # pylint:disable=protected-access
        return omscls

    @classmethod
    def from_cif(cls, path: Union[str, Path]):
        """Create a MOFChecker instance from a CIF file

        Args:
            path (Union[str, Path]): Path to string file

        Returns:
            MOFChecker: Instance of MOFChecker
        """
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            cifparser = CifParser(path)
            structure = cifparser.get_structures()[0]
            omscls = cls(structure)
            omscls._set_filename(path)  # pylint:disable=protected-access
            return omscls

    def _set_cnn(self, method="JmolNN", porous_adjustment: bool = True):
        self.porous_adjustment = porous_adjustment
        if self._cnn is None or self._cnn_method != method.lower():
            if method.lower() == "crystalnn":
                self._cnn = CrystalNN(porous_adjustment=self.porous_adjustment)
            else:
                self._cnn = JmolNN()
            self._cnn_method = method.lower()

    def get_cn(self, site_index: int) -> int:
        """Compute coordination number (CN) for site with CrystalNN method

        Args:
            site_index (int): index of site in pymatgen Structure

        Returns:
            int: Coordination number
        """
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            self._set_cnn()
            return self._cnn.get_cn(self.structure, site_index)

    def _get_ops_for_site(self, site_index):
        cn = self.get_cn(site_index)  # pylint:disable=invalid-name
        try:
            names = OP_DEF[cn]["names"]
            is_open = OP_DEF[cn]["open"]
            weights = OP_DEF[cn]["weights"]
            lsop = LocalStructOrderParams(names)
            return (
                cn,
                names,
                lsop.get_order_parameters(self.structure, site_index),
                is_open,
                weights,
            )
        except KeyError:
            # For a bit more fine grained error messages
            if cn <= 3:  # pylint:disable=no-else-raise
                raise LowCoordinationNumber(
                    "Coordination number {} is low \
                        and order parameters undefined".format(
                        cn
                    )
                )
            elif cn > 8:
                raise HighCoordinationNumber(
                    "Coordination number {} is high \
                        and order parameters undefined".format(
                        cn
                    )
                )

            return cn, None, None, None, None

    def is_site_open(self, site_index: int) -> bool:
        """Check for a site if is open (based on the values of
        some coordination geomeetry fingerprints)

        Args:
            site_index (int): Index of the site in the structure

        Returns:
            bool: True if site is open
        """
        if site_index not in self._open_indices:
            try:
                _, names, lsop, is_open, weights = self._get_ops_for_site(site_index)
                print(list(zip(names, lsop)))
                site_open = MOFChecker._check_if_open(lsop, is_open, weights)
                if site_open:
                    self._open_indices.add(site_index)
                return site_open
            except LowCoordinationNumber:
                return True
            except HighCoordinationNumber:
                return None
        return True

    @staticmethod
    def _check_if_open(lsop, is_open, weights, threshold: float = 0.5):
        if lsop is not None:
            if is_open is None:
                return False
            lsop = np.array(lsop) * np.array(weights)
            open_contributions = lsop[is_open].sum()
            close_contributions = lsop.sum() - open_contributions
            return (
                open_contributions / (open_contributions + close_contributions)
                > threshold
            )
        return None

    def _get_metal_descriptors_for_site(self, site_index: int):
        metal = str(self.structure[site_index].species)
        try:
            (
                cn,  # pylint:disable=invalid-name
                names,
                lsop,
                is_open,
                weights,
            ) = self._get_ops_for_site(site_index)
            site_open = MOFChecker._check_if_open(lsop, is_open, weights)
            if site_open:
                self._open_indices.add(site_index)
            descriptors = {
                "metal": metal,
                "lsop": dict(zip(names, lsop)),
                "open": site_open,
                "cn": cn,
            }
        except LowCoordinationNumber:
            descriptors = {"metal": metal, "lsop": None, "open": True, "cn": None}
        except HighCoordinationNumber:
            descriptors = {"metal": metal, "lsop": None, "open": None, "cn": None}
        return descriptors

    def _has_stray_molecules(self) -> bool:
        self._set_cnn()
        sgraph = StructureGraph.with_local_env_strategy(self.structure, self._cnn)
        molecules = get_subgraphs_as_molecules_all(sgraph)
        if len(molecules) > 0:
            return True
        return False

    def get_mof_descriptors(self) -> dict:
        """Run most of the sanity checks
        and get a dictionary with the result

        Returns:
            dict: result of overall checks
        """
        result_dict = {
            "name": self.name,
            "path": self._filename,
            "has_oms": self.has_oms,
            "has_carbon": self.has_carbon,
            "has_hydrogen": self.has_hydrogen,
            "has_atomic_overlaps": self.has_atomic_overlaps,
            "has_overcoordinated_c": self.has_overvalent_c,
            "has_overcoordinated_n": self.has_overvalent_n,
            "has_overcoordinated_h": self.has_overvalent_h,
            "has_undercoordinated_c": self.has_undercoordinated_c,
            "has_undercoordinated_n": self.has_undercoordinated_n,
            "has_metal": self.has_metal,
            "has_lone_atom": self.has_lone_atom,
            "has_lone_molecule": self.has_lone_molecule,
            "density": self.density,
        }
        return result_dict

    def get_metal_descriptors_for_site(self, site_index: int) -> dict:
        """Computes the checks for one metal site"""
        if not self.has_metal:
            raise NoMetal
        return self._get_metal_descriptors_for_site(site_index)

    def _get_metal_descriptors(self):
        descriptordict = {}
        for site_index in self.metal_indices:
            descriptordict[site_index] = self._get_metal_descriptors_for_site(
                site_index
            )

        self.metal_features = descriptordict

        return descriptordict

    def get_metal_descriptors(self) -> dict:
        """Return local structure order parameters for coordination number (CN),
        element string and wheter site is open or not. Key is the site index.

        Raises:
            NoMetal: If no metal can be found in the structure

        Returns:
            dict: Key is the site index.
        """
        if not self.has_metal:
            raise NoMetal
        return self._get_metal_descriptors()

    @property
    def has_metal(self):
        """Checks if there is at least one metal in the structure"""
        if self.metal_indices:
            return True
        return False

    @property
    def has_oms(self) -> bool:
        """True if the structure contains open metal sites (OMS).
        Also returns True in case of low coordination numbers (CN <=3)
        which typically also means open coordination for MOFs.
        For high coordination numbers, for which we do not have a good order
        parameter for open structures. For this reason we return None even though
        this might change in a future release.

        Raises:
            NoMetal: Raised if the structure contains no metal

        Returns:
            [bool]: True if the structure contains OMS
        """
        if not self.has_metal:
            raise NoMetal("This structure does not contain a metal")
        if self._has_oms is not None:  # pylint:disable=no-else-return
            return self._has_oms
        else:
            for site_index in self.metal_indices:
                if self.is_site_open(site_index):
                    self._has_oms = True
                    return True
            self._has_oms = False
            return False
示例#23
0
def get_NNs_pm(atoms, site_idx, NN_method):
    """
	Get coordinating atoms to the adsorption site

	Args:
		atoms (Atoms object): atoms object of MOF

		site_idx (int): ASE index of adsorption site
		
		NN_method (string): string representing the desired Pymatgen
		nearest neighbor algorithm:
		refer to http://pymatgen.org/_modules/pymatgen/analysis/local_env.html
	
	Returns:
		neighbors_idx (list of ints): ASE indices of coordinating atoms
	"""
    #Convert ASE Atoms object to Pymatgen Structure object
    bridge = pm_ase.AseAtomsAdaptor()
    struct = bridge.get_structure(atoms)

    #Select Pymatgen NN algorithm
    NN_method = NN_method.lower()
    if NN_method == 'vire':
        nn_object = MinimumVIRENN()
    elif NN_method == 'voronoi':
        nn_object = VoronoiNN()
    elif NN_method == 'jmol':
        nn_object = JmolNN()
    elif NN_method == 'min_dist':
        nn_object = MinimumDistanceNN()
    elif NN_method == 'okeeffe':
        nn_object = MinimumOKeeffeNN()
    elif NN_method == 'brunner_real':
        nn_object = BrunnerNN_real()
    elif NN_method == 'brunner_recpirocal':
        nn_object = BrunnerNN_reciprocal()
    elif NN_method == 'brunner_relative':
        nn_object = BrunnerNN_relative()
    elif NN_method == 'econ':
        nn_object = EconNN()
    elif NN_method == 'dict':
        #requires a cutoff dictionary located in the pwd
        nn_object = CutOffDictNN(cut_off_dict='cut_off_dict.txt')
    elif NN_method == 'critic2':
        nn_object = Critic2NN()
    elif NN_method == 'openbabel':
        nn_object = OpenBabelNN()
    elif NN_method == 'covalent':
        nn_object = CovalentBondNN()
    elif NN_method == 'crystal':
        nn_object = CrystalNN(porous_adjustment=True)
    elif NN_method == 'crystal_nonporous':
        nn_object = CrystalNN(porous_adjustment=False)
    else:
        raise ValueError('Invalid NN algorithm specified')

    #Find coordinating atoms
    with warnings.catch_warnings():
        warnings.simplefilter('ignore')
        neighbors = nn_object.get_nn_info(struct, site_idx)
    neighbors_idx = []
    for neighbor in neighbors:
        neighbors_idx.append(neighbor['site_index'])

    return neighbors_idx