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)
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)
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)
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"}, )
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)
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)
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))
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))
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, )
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})