Пример #1
0
    def test_subset(self):
        sm = StructureMatcher(
            ltol=0.2,
            stol=0.3,
            angle_tol=5,
            primitive_cell=False,
            scale=True,
            attempt_supercell=False,
            allow_subset=True,
        )
        l = Lattice.orthorhombic(10, 20, 30)
        s1 = Structure(l, ["Si", "Si", "Ag"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
        s2 = Structure(l, ["Si", "Ag"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
        result = sm.get_s2_like_s1(s1, s2)

        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, [0, 0, 0.1])), 1)
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, [0.7, 0.4, 0.5])), 1)

        # test with fewer species in s2
        s1 = Structure(l, ["Si", "Ag", "Si"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
        s2 = Structure(l, ["Si", "Si"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
        result = sm.get_s2_like_s1(s1, s2)
        mindists = np.min(s1.lattice.get_all_distances(s1.frac_coords, result.frac_coords), axis=0)
        self.assertLess(np.max(mindists), 1e-6)

        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, [0, 0, 0.1])), 1)
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, [0.7, 0.4, 0.5])), 1)

        # test with not enough sites in s1
        # test with fewer species in s2
        s1 = Structure(l, ["Si", "Ag", "Cl"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
        s2 = Structure(l, ["Si", "Si"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
        self.assertEqual(sm.get_s2_like_s1(s1, s2), None)
Пример #2
0
    def test_disordered_primitive_to_ordered_supercell(self):
        sm_atoms = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5,
                                    primitive_cell=False, scale=True,
                                    attempt_supercell=True,
                                    allow_subset=True,
                                    supercell_size = 'num_atoms',
                                    comparator=OrderDisorderElementComparator())
        sm_sites = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5,
                                    primitive_cell=False, scale=True,
                                    attempt_supercell=True,
                                    allow_subset=True,
                                    supercell_size = 'num_sites',
                                    comparator=OrderDisorderElementComparator())
        lp = Lattice.orthorhombic(10, 20, 30)
        pcoords = [[0,   0,   0],
                   [0.5, 0.5, 0.5]]
        ls = Lattice.orthorhombic(20,20,30)
        scoords = [[0,    0,   0],
                   [0.75, 0.5, 0.5]]
        prim = Structure(lp, [{'Na':0.5}, {'Cl':0.5}], pcoords)
        supercell = Structure(ls, ['Na', 'Cl'], scoords)
        supercell.make_supercell([[-1,1,0],[0,1,1],[1,0,0]])

        self.assertFalse(sm_sites.fit(prim, supercell))
        self.assertTrue(sm_atoms.fit(prim, supercell))

        self.assertRaises(ValueError, sm_atoms.get_s2_like_s1, prim, supercell)
        self.assertEqual(len(sm_atoms.get_s2_like_s1(supercell, prim)), 4)
Пример #3
0
    def test_subset(self):
        sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, 
                              primitive_cell=False, scale=True, 
                              attempt_supercell=False,
                              allow_subset=True)
        l = Lattice.orthorhombic(10, 20, 30)
        s1 = Structure(l, ['Si', 'Si', 'Ag'], 
                       [[0,0,0.1],[0,0,0.2],[.7,.4,.5]])
        s2 = Structure(l, ['Si', 'Ag'], 
                       [[0,0.1,0],[-.7,.5,.4]])
        result = sm.get_s2_like_s1(s1, s2)
        
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                    [0,0,0.1])), 1)
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                    [0.7,0.4,0.5])), 1)

        #test with fewer species in s2
        s1 = Structure(l, ['Si', 'Ag', 'Si'], 
                       [[0,0,0.1],[0,0,0.2],[.7,.4,.5]])
        s2 = Structure(l, ['Si', 'Si'], 
                       [[0,0.1,0],[-.7,.5,.4]])
        result = sm.get_s2_like_s1(s1, s2)
        
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                    [0,0,0.1])), 1)
        self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                    [0.7,0.4,0.5])), 1)
        
        #test with not enough sites in s1
        #test with fewer species in s2
        s1 = Structure(l, ['Si', 'Ag', 'Cl'], 
                       [[0,0,0.1],[0,0,0.2],[.7,.4,.5]])
        s2 = Structure(l, ['Si', 'Si'], 
                       [[0,0.1,0],[-.7,.5,.4]])
        self.assertEqual(sm.get_s2_like_s1(s1, s2), None)
Пример #4
0
    def test_ignore_species(self):
        s1 = Structure.from_file(os.path.join(test_dir, "LiFePO4.cif"))
        s2 = Structure.from_file(os.path.join(test_dir, "POSCAR"))
        m = StructureMatcher(ignored_species=["Li"], primitive_cell=False, attempt_supercell=True)
        self.assertTrue(m.fit(s1, s2))
        self.assertTrue(m.fit_anonymous(s1, s2))
        groups = m.group_structures([s1, s2])
        self.assertEqual(len(groups), 1)
        s2.make_supercell((2, 1, 1))
        ss1 = m.get_s2_like_s1(s2, s1, include_ignored_species=True)
        self.assertAlmostEqual(ss1.lattice.a, 20.820740000000001)
        self.assertEqual(ss1.composition.reduced_formula, "LiFePO4")

        self.assertEqual(
            {k.symbol: v.symbol for k, v in m.get_best_electronegativity_anonymous_mapping(s1, s2).items()},
            {"Fe": "Fe", "P": "P", "O": "O"},
        )
Пример #5
0
 def test_get_s2_like_s1(self):
     sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5, 
                           primitive_cell=False, scale=True, 
                           attempt_supercell=True)
     l = Lattice.orthorhombic(1, 2, 3)
     s1 = Structure(l, ['Si', 'Si', 'Ag'], 
                    [[0,0,0.1],[0,0,0.2],[.7,.4,.5]])
     s1.make_supercell([2,1,1])
     s2 = Structure(l, ['Si', 'Si', 'Ag'], 
                    [[0,0.1,0],[0,0.1,-0.95],[-.7,.5,.375]])
     result = sm.get_s2_like_s1(s1, s2)
     
     self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                 [0.35,0.4,0.5])), 1)
     self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                 [0,0,0.125])), 1)
     self.assertEqual(len(find_in_coord_list_pbc(result.frac_coords, 
                                                 [0,0,0.175])), 1)
Пример #6
0
    def test_get_s2_large_s2(self):
        sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5,
                              primitive_cell=False, scale=False,
                              attempt_supercell=True, allow_subset=False,
                              supercell_size='volume')

        l = Lattice.orthorhombic(1, 2, 3)
        s1 = Structure(l, ['Ag', 'Si', 'Si'],
                       [[.7,.4,.5],[0,0,0.1],[0,0,0.2]])

        l2 = Lattice.orthorhombic(1.01, 2.01, 3.01)
        s2 = Structure(l2, ['Si', 'Si', 'Ag'],
                       [[0,0.1,-0.95],[0,0.1,0],[-.7,.5,.375]])
        s2.make_supercell([[0,-1,0],[1,0,0],[0,0,1]])

        result = sm.get_s2_like_s1(s1, s2)

        for x,y in zip(s1, result):
            self.assertLess(x.distance(y), 0.08)
Пример #7
0
    def test_supercell_subsets(self):
        sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5,
                              primitive_cell=False, scale=True,
                              attempt_supercell=True, allow_subset=True,
                              supercell_size='volume')
        sm_no_s = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5,
                              primitive_cell=False, scale=True,
                              attempt_supercell=True, allow_subset=False,
                              supercell_size='volume')
        l = Lattice.orthorhombic(1, 2, 3)
        s1 = Structure(l, ['Ag', 'Si', 'Si'],
                       [[.7,.4,.5],[0,0,0.1],[0,0,0.2]])
        s1.make_supercell([2,1,1])
        s2 = Structure(l, ['Si', 'Si', 'Ag'],
                       [[0,0.1,-0.95],[0,0.1,0],[-.7,.5,.375]])

        shuffle = [0,2,1,3,4,5]
        s1 = Structure.from_sites([s1[i] for i in shuffle])

        #test when s1 is exact supercell of s2
        result = sm.get_s2_like_s1(s1, s2)
        for a, b in zip(s1, result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(s1, s2))
        self.assertTrue(sm.fit(s2, s1))
        self.assertTrue(sm_no_s.fit(s1, s2))
        self.assertTrue(sm_no_s.fit(s2, s1))

        rms = (0.048604032430991401, 0.059527539448807391)
        self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2), rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2, s1), rms))

        #test when the supercell is a subset of s2
        subset_supercell = s1.copy()
        del subset_supercell[0]
        result = sm.get_s2_like_s1(subset_supercell, s2)
        self.assertEqual(len(result), 6)
        for a, b in zip(subset_supercell, result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(subset_supercell, s2))
        self.assertTrue(sm.fit(s2, subset_supercell))
        self.assertFalse(sm_no_s.fit(subset_supercell, s2))
        self.assertFalse(sm_no_s.fit(s2, subset_supercell))

        rms = (0.053243049896333279, 0.059527539448807336)
        self.assertTrue(np.allclose(sm.get_rms_dist(subset_supercell, s2), rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2, subset_supercell), rms))

        #test when s2 (once made a supercell) is a subset of s1
        s2_missing_site = s2.copy()
        del s2_missing_site[1]
        result = sm.get_s2_like_s1(s1, s2_missing_site)
        for a, b in zip((s1[i] for i in (0, 2, 4, 5)), result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(s1, s2_missing_site))
        self.assertTrue(sm.fit(s2_missing_site, s1))
        self.assertFalse(sm_no_s.fit(s1, s2_missing_site))
        self.assertFalse(sm_no_s.fit(s2_missing_site, s1))

        rms = (0.029763769724403633, 0.029763769724403987)
        self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2_missing_site), rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2_missing_site, s1), rms))
Пример #8
0
    def test_supercell_subsets(self):
        sm = StructureMatcher(ltol=0.2,
                              stol=0.3,
                              angle_tol=5,
                              primitive_cell=False,
                              scale=True,
                              attempt_supercell=True,
                              allow_subset=True,
                              supercell_size='volume')
        sm_no_s = StructureMatcher(ltol=0.2,
                                   stol=0.3,
                                   angle_tol=5,
                                   primitive_cell=False,
                                   scale=True,
                                   attempt_supercell=True,
                                   allow_subset=False,
                                   supercell_size='volume')
        l = Lattice.orthorhombic(1, 2, 3)
        s1 = Structure(l, ['Ag', 'Si', 'Si'],
                       [[.7, .4, .5], [0, 0, 0.1], [0, 0, 0.2]])
        s1.make_supercell([2, 1, 1])
        s2 = Structure(l, ['Si', 'Si', 'Ag'],
                       [[0, 0.1, -0.95], [0, 0.1, 0], [-.7, .5, .375]])

        shuffle = [0, 2, 1, 3, 4, 5]
        s1 = Structure.from_sites([s1[i] for i in shuffle])

        #test when s1 is exact supercell of s2
        result = sm.get_s2_like_s1(s1, s2)
        for a, b in zip(s1, result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(s1, s2))
        self.assertTrue(sm.fit(s2, s1))
        self.assertTrue(sm_no_s.fit(s1, s2))
        self.assertTrue(sm_no_s.fit(s2, s1))

        rms = (0.048604032430991401, 0.059527539448807391)
        self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2), rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2, s1), rms))

        #test when the supercell is a subset of s2
        subset_supercell = s1.copy()
        del subset_supercell[0]
        result = sm.get_s2_like_s1(subset_supercell, s2)
        self.assertEqual(len(result), 6)
        for a, b in zip(subset_supercell, result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(subset_supercell, s2))
        self.assertTrue(sm.fit(s2, subset_supercell))
        self.assertFalse(sm_no_s.fit(subset_supercell, s2))
        self.assertFalse(sm_no_s.fit(s2, subset_supercell))

        rms = (0.053243049896333279, 0.059527539448807336)
        self.assertTrue(np.allclose(sm.get_rms_dist(subset_supercell, s2),
                                    rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2, subset_supercell),
                                    rms))

        #test when s2 (once made a supercell) is a subset of s1
        s2_missing_site = s2.copy()
        del s2_missing_site[1]
        result = sm.get_s2_like_s1(s1, s2_missing_site)
        for a, b in zip((s1[i] for i in (0, 2, 4, 5)), result):
            self.assertTrue(a.distance(b) < 0.08)
            self.assertEqual(a.species_and_occu, b.species_and_occu)

        self.assertTrue(sm.fit(s1, s2_missing_site))
        self.assertTrue(sm.fit(s2_missing_site, s1))
        self.assertFalse(sm_no_s.fit(s1, s2_missing_site))
        self.assertFalse(sm_no_s.fit(s2_missing_site, s1))

        rms = (0.029763769724403633, 0.029763769724403987)
        self.assertTrue(np.allclose(sm.get_rms_dist(s1, s2_missing_site), rms))
        self.assertTrue(np.allclose(sm.get_rms_dist(s2_missing_site, s1), rms))
Пример #9
0
class ComputedEntryPath(FullPathMapper):
    """
    Generate the full migration network using computed entires for intercollation andvacancy limits
    - Map the relaxed sites of a material back to the empty host lattice
    - Apply symmetry operations of the empty lattice to obtain the other positions of the intercollated atom
    - Get the symmetry inequivalent hops
    - Get the migration barriers for each inequivalent hop
    """
    def __init__(
        self,
        base_struct_entry,
        single_cat_entries,
        migrating_specie,
        base_aeccar=None,
        max_path_length=4,
        ltol=0.2,
        stol=0.3,
        symprec=0.1,
        angle_tol=5,
        full_sites_struct=None,
    ):
        """
        Pass in a entries for analysis

        Args:
          base_struct_entry: the structure without a working ion for us to analyze the migration
          single_cat_entries: list of structures containing a single cation at different positions
          base_aeccar: Chgcar object that contains the AECCAR0 + AECCAR2 (Default value = None)
          migration_specie: a String symbol or Element for the cation. (Default value = 'Li')
          ltol: parameter for StructureMatcher (Default value = 0.2)
          stol: parameter for StructureMatcher (Default value = 0.3)
          symprec: parameter for SpacegroupAnalyzer (Default value = 0.3)
          angle_tol: parameter for StructureMatcher (Default value = 5)
        """

        self.single_cat_entries = single_cat_entries
        self.base_struct_entry = base_struct_entry
        self.base_aeccar = base_aeccar
        self.migrating_specie = migrating_specie
        self.ltol = ltol
        self.stol = stol
        self.symprec = symprec
        self.angle_tol = angle_tol
        self.angle_tol = angle_tol
        self._tube_radius = None
        self.full_sites_struct = full_sites_struct
        self.sm = StructureMatcher(
            comparator=ElementComparator(),
            primitive_cell=False,
            ignored_species=[migrating_specie],
            ltol=ltol,
            stol=stol,
            angle_tol=angle_tol,
        )

        logger.debug("See if the structures all match")
        fit_ents = []
        if full_sites_struct:
            self.full_sites = full_sites_struct
            self.base_structure_full_sites = self.full_sites.copy()
            self.base_structure_full_sites.sites.extend(
                self.base_struct_entry.structure.sites)
        else:
            for ent in self.single_cat_entries:
                if self.sm.fit(self.base_struct_entry.structure,
                               ent.structure):
                    fit_ents.append(ent)
            self.single_cat_entries = fit_ents

            self.translated_single_cat_entries = list(
                map(self.match_ent_to_base, self.single_cat_entries))
            self.full_sites = self.get_full_sites()
            self.base_structure_full_sites = self.full_sites.copy()
            self.base_structure_full_sites.sites.extend(
                self.base_struct_entry.structure.sites)

        # Initialize
        super(ComputedEntryPath, self).__init__(
            structure=self.base_structure_full_sites,
            migrating_specie=migrating_specie,
            max_path_length=max_path_length,
            symprec=symprec,
            vac_mode=False,
            name=base_struct_entry.entry_id,
        )

        self.populate_edges_with_migration_paths()
        self.group_and_label_hops()
        self._populate_unique_hops_dict()
        if base_aeccar:
            self._setup_grids()

    def from_dbs(self):
        """
        Populate the object using entries from MP-like databases
        """

    def _from_dbs(self):
        """
        Populate the object using entries from MP-like databases

        """

    def match_ent_to_base(self, ent):
        """
        Transform the structure of one entry to match the base structure

        Args:
          ent:

        Returns:
          ComputedStructureEntry: entry with modified structure

        """
        new_ent = deepcopy(ent)
        new_struct = self.sm.get_s2_like_s1(self.base_struct_entry.structure,
                                            ent.structure)
        new_ent.structure = new_struct
        return new_ent

    def get_full_sites(self):
        """
        Get each group of symmetry inequivalent sites and combine them

        Args:

        Returns: a Structure with all possible Li sites, the enregy of the structure is stored as a site property

        """
        res = []
        for itr in self.translated_single_cat_entries:
            sub_site_list = get_all_sym_sites(
                itr,
                self.base_struct_entry,
                self.migrating_specie,
                symprec=self.symprec,
                angle_tol=self.angle_tol,
            )
            # ic(sub_site_list._sites)
            res.extend(sub_site_list._sites)
        # check to see if the sites collide with the base struture
        filtered_res = []
        for itr in res:
            col_sites = self.base_struct_entry.structure.get_sites_in_sphere(
                itr.coords, BASE_COLLISION_R)
            if len(col_sites) == 0:
                filtered_res.append(itr)
        res = Structure.from_sites(filtered_res)
        if len(res) > 1:
            res.merge_sites(tol=SITE_MERGE_R, mode="average")
        return res

    def _setup_grids(self):
        """Populate the internal varialbes used for defining the grid points in the charge density analysis"""

        # set up the grid
        aa = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(0)),
                         endpoint=False)
        bb = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(1)),
                         endpoint=False)
        cc = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(2)),
                         endpoint=False)
        # move the grid points to the center
        aa, bb, dd = map(_shift_grid, [aa, bb, cc])

        # mesh grid for each unit cell
        AA, BB, CC = np.meshgrid(aa, bb, cc, indexing="ij")

        # should be using a mesh grid of 5x5x5 (using 3x3x3 misses some fringe cases)
        # but using 3x3x3 is much faster and only crops the cyliners in some rare case
        # if you keep the tube_radius small then this is not a big deal
        IMA, IMB, IMC = np.meshgrid([-1, 0, 1], [-1, 0, 1], [-1, 0, 1],
                                    indexing="ij")

        # store these
        self._uc_grid_shape = AA.shape
        self._fcoords = np.vstack([AA.flatten(), BB.flatten(), CC.flatten()]).T
        self._images = np.vstack([IMA.flatten(),
                                  IMB.flatten(),
                                  IMC.flatten()]).T

    def _dist_mat(self, pos_frac):
        # return a matrix that contains the distances to pos_frac
        aa = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(0)),
                         endpoint=False)
        bb = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(1)),
                         endpoint=False)
        cc = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(2)),
                         endpoint=False)
        aa, bb, cc = map(_shift_grid, [aa, bb, cc])
        AA, BB, CC = np.meshgrid(aa, bb, cc, indexing="ij")
        dist_from_pos = self.base_aeccar.structure.lattice.get_all_distances(
            fcoords1=np.vstack([AA.flatten(),
                                BB.flatten(),
                                CC.flatten()]).T,
            fcoords2=pos_frac,
        )
        return dist_from_pos.reshape(AA.shape)

    def _get_pathfinder_from_hop(self, migration_path, n_images=20):
        # get migration pathfinder objects which contains the paths
        ipos = migration_path.isite.frac_coords
        epos = migration_path.esite.frac_coords
        mpos = migration_path.esite.frac_coords

        start_struct = self.base_aeccar.structure.copy()
        end_struct = self.base_aeccar.structure.copy()
        mid_struct = self.base_aeccar.structure.copy()

        # the moving ion is always inserted on the zero index
        start_struct.insert(0,
                            self.migrating_specie,
                            ipos,
                            properties=dict(magmom=0))
        end_struct.insert(0,
                          self.migrating_specie,
                          epos,
                          properties=dict(magmom=0))
        mid_struct.insert(0,
                          self.migrating_specie,
                          mpos,
                          properties=dict(magmom=0))

        chgpot = ChgcarPotential(self.base_aeccar, normalize=False)
        npf = NEBPathfinder(
            start_struct,
            end_struct,
            relax_sites=[0],
            v=chgpot.get_v(),
            n_images=n_images,
            mid_struct=mid_struct,
        )
        return npf

    def _get_avg_chg_at_max(self,
                            migration_path,
                            radius=None,
                            chg_along_path=False,
                            output_positions=False):
        """obtain the maximum average charge along the path

        Args:
            migration_path (MigrationPath): MigrationPath object that represents a given hop
            radius (float, optional): radius of sphere to perform the average.
                    Defaults to None, which used the _tube_radius instead
            chg_along_path (bool, optional): If True, also return the entire list of average
                    charges along the path for plotting.
                    Defaults to False.
            output_positions (bool, optional): If True, also return the entire list of average
                    charges along the path for plotting.
                    Defaults to False.

        Returns:
            [float]: maximum of the charge density, (optional: entire list of charge density)
        """
        if radius is None:
            rr = self._tube_radius
        if not rr > 0:
            raise ValueError("The integration radius must be positive.")

        npf = self._get_pathfinder_from_hop(migration_path)
        # get the charge in a sphere around each point
        centers = [image.sites[0].frac_coords for image in npf.images]
        avg_chg = []
        for ict in centers:
            dist_mat = self._dist_mat(ict)
            mask = dist_mat < rr
            vol_sphere = self.base_aeccar.structure.volume * (
                mask.sum() / self.base_aeccar.ngridpts)
            avg_chg.append(
                np.sum(self.base_aeccar.data["total"] * mask) /
                self.base_aeccar.ngridpts / vol_sphere)
        if output_positions:
            return max(avg_chg), avg_chg, centers
        elif chg_along_path:
            return max(avg_chg), avg_chg
        else:
            return max(avg_chg)

    def _get_chg_between_sites_tube(self,
                                    migration_path,
                                    mask_file_seedname=None):
        """
        Calculate the amount of charge that a migrating ion has to move through in order to complete a hop

        Args:
            migration_path: MigrationPath object that represents a given hop
            mask_file_seedname(string): seedname for output of the migration path masks (for debugging and
                visualization) (Default value = None)

        Returns:
            float: The total charge density in a tube that connects two sites of a given edges of the graph

        """
        try:
            self._tube_radius
        except NameError:
            logger.warning(
                "The radius of the tubes for charge analysis need to be defined first."
            )
        ipos = migration_path.isite.frac_coords
        epos = migration_path.esite.frac_coords
        if not self.base_aeccar:
            return 0

        cart_ipos = np.dot(ipos, self.base_aeccar.structure.lattice.matrix)
        cart_epos = np.dot(epos, self.base_aeccar.structure.lattice.matrix)
        pbc_mask = np.zeros(self._uc_grid_shape, dtype=bool).flatten()
        for img in self._images:
            grid_pos = np.dot(self._fcoords + img,
                              self.base_aeccar.structure.lattice.matrix)
            proj_on_line = np.dot(
                grid_pos - cart_ipos, cart_epos -
                cart_ipos) / (np.linalg.norm(cart_epos - cart_ipos))
            dist_to_line = np.linalg.norm(
                np.cross(grid_pos - cart_ipos, cart_epos - cart_ipos) /
                (np.linalg.norm(cart_epos - cart_ipos)),
                axis=-1,
            )

            mask = ((proj_on_line >= 0) *
                    (proj_on_line < np.linalg.norm(cart_epos - cart_ipos)) *
                    (dist_to_line < self._tube_radius))
            pbc_mask = pbc_mask + mask
        pbc_mask = pbc_mask.reshape(self._uc_grid_shape)

        if mask_file_seedname:
            mask_out = VolumetricData(
                structure=self.base_aeccar.structure.copy(),
                data={"total": self.base_aeccar.data["total"]},
            )
            mask_out.structure.insert(0, "X", ipos)
            mask_out.structure.insert(0, "X", epos)
            mask_out.data["total"] = pbc_mask
            isym = self.symm_structure.wyckoff_symbols[migration_path.iindex]
            esym = self.symm_structure.wyckoff_symbols[migration_path.eindex]
            mask_out.write_file("{}_{}_{}_tot({:0.2f}).vasp".format(
                mask_file_seedname, isym, esym, mask_out.data["total"].sum()))

        return (self.base_aeccar.data["total"][pbc_mask].sum() /
                self.base_aeccar.ngridpts / self.base_aeccar.structure.volume)

    def populate_edges_with_chg_density_info(self, tube_radius=1):
        self._tube_radius = tube_radius
        for k, v in self.unique_hops.items():
            # charge in tube
            chg_tot = self._get_chg_between_sites_tube(v["hop"])
            self.add_data_to_similar_edges(k, {"chg_total": chg_tot})

            # max charge in sphere
            max_chg, avg_chg_list, frac_coords_list = self._get_avg_chg_at_max(
                v["hop"], chg_along_path=True, output_positions=True)
            images = [{
                "position": ifrac,
                "average_charge": ichg
            } for ifrac, ichg in zip(frac_coords_list, avg_chg_list)]
            v.update(
                dict(
                    chg_total=chg_tot,
                    max_avg_chg=max_chg,
                    images=images,
                ))
            self.add_data_to_similar_edges(k, {"max_avg_chg": max_chg})

    def get_least_chg_path(self):
        """
        obtain an intercollating pathway through the material that has the least amount of charge
        Returns:
            list of hops
        """
        min_chg = 100000000
        min_path = []
        all_paths = self.get_intercalating_path()
        for path in all_paths:
            sum_chg = np.sum([hop[2]["chg_total"] for hop in path])
            sum_length = np.sum([hop[2]["hop"].length for hop in path])
            avg_chg = sum_chg / sum_length
            if avg_chg < min_chg:
                min_chg = sum_chg
                min_path = path
        return min_path

    def get_summary_dict(self):
        """
        Dictionary format, for saving to database
        """
        hops = []
        for u, v, d in self.s_graph.graph.edges(data=True):
            dd = defaultdict(lambda: None)
            dd.update(d)
            hops.append(
                dict(
                    hop_label=dd["hop_label"],
                    iinddex=u,
                    einddex=v,
                    to_jimage=dd["to_jimage"],
                    ipos=dd["ipos"],
                    epos=dd["epos"],
                    ipos_cart=dd["ipos_cart"],
                    epos_cart=dd["epos_cart"],
                    max_avg_chg=dd["max_avg_chg"],
                    chg_total=dd["chg_total"],
                ))
        unique_hops = []
        for k, d in self.unique_hops.items():
            dd = defaultdict(lambda: None)
            dd.update(d)
            unique_hops.append(
                dict(
                    hop_label=dd["hop_label"],
                    iinddex=dd["iinddex"],
                    einddex=dd["einddex"],
                    to_jimage=dd["to_jimage"],
                    ipos=dd["ipos"],
                    epos=dd["epos"],
                    ipos_cart=dd["ipos_cart"],
                    epos_cart=dd["epos_cart"],
                    max_avg_chg=dd["max_avg_chg"],
                    chg_total=dd["chg_total"],
                    images=dd["images"],
                ))
        unique_hops = sorted(unique_hops, key=lambda x: x["hop_label"])
        return dict(
            base_task_id=self.base_struct_entry.entry_id,
            base_structure=self.base_struct_entry.structure.as_dict(),
            inserted_ids=[ent.entry_id for ent in self.single_cat_entries],
            migrating_specie=self.migrating_specie.name,
            max_path_length=self.max_path_length,
            ltol=self.ltol,
            stol=self.stol,
            full_sites_struct=self.full_sites.as_dict(),
            angle_tol=self.angle_tol,
            hops=hops,
            unique_hops=unique_hops,
        )
Пример #10
0
class ComputedEntryPath(FullPathMapper):
    """
    Generate the full migration network using computed entires for intercollation andvacancy limits
    - Map the relaxed sites of a material back to the empty host lattice
    - Apply symmetry operations of the empty lattice to obtain the other positions of the intercollated atom
    - Get the symmetry inequivalent hops
    - Get the migration barriers for each inequivalent hop
    """
    def __init__(self,
                 base_struct_entry,
                 single_cat_entries,
                 migrating_specie,
                 base_aeccar=None,
                 max_path_length=4,
                 ltol=0.2,
                 stol=0.3,
                 full_sites_struct=None,
                 angle_tol=5):
        """
        Pass in a entries for analysis

        Args:
          base_struct_entry: the structure without a working ion for us to analyze the migration
          single_cat_entries: list of structures containing a single cation at different positions
          base_aeccar: Chgcar object that contains the AECCAR0 + AECCAR2 (Default value = None)
          migration_specie: a String symbol or Element for the cation. (Default value = 'Li')
          ltol: parameter for StructureMatcher (Default value = 0.2)
          stol: parameter for StructureMatcher (Default value = 0.3)
          angle_tol: parameter for StructureMatcher (Default value = 5)
        """

        self.single_cat_entries = single_cat_entries
        self.base_struct_entry = base_struct_entry
        self.base_aeccar = base_aeccar
        self.migrating_specie = migrating_specie
        self._tube_radius = 0
        self.sm = StructureMatcher(comparator=ElementComparator(),
                                   primitive_cell=False,
                                   ignored_species=[migrating_specie],
                                   ltol=ltol,
                                   stol=stol,
                                   angle_tol=angle_tol)

        logger.debug('See if the structures all match')
        fit_ents = []
        if full_sites_struct:
            self.full_sites = full_sites_struct
            self.base_structure_full_sites = self.full_sites.copy()
            self.base_structure_full_sites.sites.extend(
                self.base_struct_entry.structure.sites)
        else:
            for ent in self.single_cat_entries:
                if self.sm.fit(self.base_struct_entry.structure,
                               ent.structure):
                    fit_ents.append(ent)
            self.single_cat_entries = fit_ents

            self.translated_single_cat_entries = list(
                map(self.match_ent_to_base, self.single_cat_entries))
            self.full_sites = self.get_full_sites()
            self.base_structure_full_sites = self.full_sites.copy()
            self.base_structure_full_sites.sites.extend(
                self.base_struct_entry.structure.sites)

        # Initialize
        super(ComputedEntryPath,
              self).__init__(structure=self.base_structure_full_sites,
                             migrating_specie=migrating_specie,
                             max_path_length=max_path_length,
                             symprec=0.1,
                             vac_mode=False)

        self.populate_edges_with_migration_paths()
        self.group_and_label_hops()
        self.get_unique_hops_dict()
        if base_aeccar:
            self._setup_grids()

    def match_ent_to_base(self, ent):
        """
        Transform the structure of one entry to match the base structure

        Args:
          ent:

        Returns:
          ComputedStructureEntry: entry with modified structure

        """
        new_ent = deepcopy(ent)
        new_struct = self.sm.get_s2_like_s1(self.base_struct_entry.structure,
                                            ent.structure)
        new_ent.structure = new_struct
        return new_ent

    def get_full_sites(self):
        """
        Get each group of symmetry inequivalent sites and combine them

        Args:

        Returns: a Structure with all possible Li sites, the enregy of the structure is stored as a site property

        """
        res = []
        for itr in self.translated_single_cat_entries:
            sub_site_list = get_all_sym_sites(itr, self.base_struct_entry,
                                              self.migrating_specie)
            # ic(sub_site_list._sites)
            res.extend(sub_site_list._sites)
        res = Structure.from_sites(res)
        # ic(res)
        if len(res) > 1:
            res.merge_sites(tol=1.0, mode='average')
        # ic(res)
        return res

    def _setup_grids(self):
        """Populate the internal varialbes used for defining the grid points in the charge density analysis"""
        def _shift_grid(vv):
            """
            Move the grid points by half a step so that they sit in the center

            Args:
              vv: equally space grid points in 1-D

            """
            step = vv[1] - vv[0]
            vv += step / 2.

        # set up the grid
        aa = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(0)),
                         endpoint=False)
        bb = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(1)),
                         endpoint=False)
        cc = np.linspace(0,
                         1,
                         len(self.base_aeccar.get_axis_grid(2)),
                         endpoint=False)
        # move the grid points to the center
        _shift_grid(aa)
        _shift_grid(bb)
        _shift_grid(cc)

        # mesh grid for each unit cell
        AA, BB, CC = np.meshgrid(aa, bb, cc, indexing='ij')

        # should be using a mesh grid of 5x5x5 (using 3x3x3 misses some fringe cases)
        # but using 3x3x3 is much faster and only crops the cyliners in some rare case
        # if you keep the tube_radius small then this is not a big deal
        IMA, IMB, IMC = np.meshgrid([-1, 0, 1], [-1, 0, 1], [-1, 0, 1],
                                    indexing='ij')

        # store these
        self._uc_grid_shape = AA.shape
        self._fcoords = np.vstack([AA.flatten(), BB.flatten(), CC.flatten()]).T
        self._images = np.vstack([IMA.flatten(),
                                  IMB.flatten(),
                                  IMC.flatten()]).T

    def _get_chg_between_sites_tube(self,
                                    migration_path,
                                    mask_file_seedname=None):
        """
        Calculate the amount of charge that a migrating ion has to move through in order to complete a hop

        Args:
            migration_path: MigrationPath object that represents a given hop
            mask_file_seedname(string): seedname for output of the migration path masks (for debugging and
                visualization) (Default value = None)

        Returns:
            float: The total charge density in a tube that connects two sites of a given edges of the graph

        """
        try:
            self._tube_radius
        except NameError:
            logger.error(
                "The radius of the tubes for charge analysis need to be defined first."
            )
        ipos = migration_path.isite.frac_coords
        epos = migration_path.esite.frac_coords

        cart_ipos = np.dot(ipos, self.base_aeccar.structure.lattice.matrix)
        cart_epos = np.dot(epos, self.base_aeccar.structure.lattice.matrix)
        pbc_mask = np.zeros(self._uc_grid_shape, dtype=bool).flatten()
        for img in self._images:
            grid_pos = np.dot(self._fcoords + img,
                              self.base_aeccar.structure.lattice.matrix)
            proj_on_line = np.dot(
                grid_pos - cart_ipos, cart_epos -
                cart_ipos) / (np.linalg.norm(cart_epos - cart_ipos))
            dist_to_line = np.linalg.norm(
                np.cross(grid_pos - cart_ipos, cart_epos - cart_ipos) /
                (np.linalg.norm(cart_epos - cart_ipos)),
                axis=-1)

            mask = (proj_on_line >= 0) * (proj_on_line < np.linalg.norm(
                cart_epos - cart_ipos)) * (dist_to_line < self._tube_radius)
            pbc_mask = pbc_mask + mask
        pbc_mask = pbc_mask.reshape(self._uc_grid_shape)

        if mask_file_seedname:
            mask_out = VolumetricData(
                structure=self.base_aeccar.structure.copy(),
                data={'total': self.base_aeccar.data['total']})
            mask_out.structure.insert(0, "X", ipos)
            mask_out.structure.insert(0, "X", epos)
            mask_out.data['total'] = pbc_mask
            isym = self.symm_structure.wyckoff_symbols[migration_path.iindex]
            esym = self.symm_structure.wyckoff_symbols[migration_path.eindex]
            mask_out.write_file('{}_{}_{}_tot({:0.2f}).vasp'.format(
                mask_file_seedname, isym, esym, mask_out.data['total'].sum()))

        return self.base_aeccar.data['total'][pbc_mask].sum(
        ) / self.base_aeccar.ngridpts / self.base_aeccar.structure.volume

    def populate_edges_with_chg_density_info(self, tube_radius=1):
        self._tube_radius = tube_radius
        for k, v in self.unique_hops.items():
            chg_tot = self._get_chg_between_sites_tube(v)
            self.add_data_to_similar_edges(k, {'chg_total': chg_tot})