def test_get_neighbors_of_site_with_index(self): self.assertEqual( len(get_neighbors_of_site_with_index(self.diamond, 0)), 4) self.assertEqual(len(get_neighbors_of_site_with_index(self.nacl, 0)), 6) self.assertEqual(len(get_neighbors_of_site_with_index(self.cscl, 0)), 8) self.assertEqual( len(get_neighbors_of_site_with_index(self.diamond, 0, delta=0.01)), 4) self.assertEqual( len(get_neighbors_of_site_with_index(self.diamond, 0, cutoff=6)), 4) self.assertEqual( len( get_neighbors_of_site_with_index(self.diamond, 0, approach="voronoi")), 4) self.assertEqual( len( get_neighbors_of_site_with_index(self.diamond, 0, approach="min_OKeeffe")), 4) self.assertEqual( len( get_neighbors_of_site_with_index(self.diamond, 0, approach="min_VIRE")), 4)
def __init__(self, crystal, L=6): # all power spectrum values up to maximum degree L ls = np.arange(2, L + 1, 1) power_spectrum = self._populate_dicts(ls) '''Loop over l to calculate each power spectrum parameter''' for l in ls: parameter_index = str(l) for index, site in enumerate(crystal): # get all nearest neighbors neighbors = get_neighbors_of_site_with_index( crystal, index, approach='voronoi') # generate free integer paramters # calculate power spectrum pl see Pl method power_spectrum['p' + parameter_index].append( self.Pl(site, neighbors, l)) spectrum_values = list(power_spectrum.values()) ''' If the list is 1 dimensional simply take the mean of the list If the list is 2 dimensional take the mean over the rows of the list''' try: # 2D case self.Power_spectrum = np.apply_along_axis(np.mean, 1, spectrum_values) except: # 1D case self.Power_spectrum = np.mean(spectrum_values)
def __init__(self, crystal, L=22): # all bond order parameters up to maximum degree L Ls = np.arange(4, L + 1, 2) '''populate a dictionary of empty lists with keys corresponding to each bond order parameter up to L''' bond_order_params = self._populate_dicts(Ls) '''loop over l to calculate each bond order parameter and store it in the dictionary''' for l in Ls: # to index the dictionary parameter_index = str(l) # iterate over each periodic site in the pymatgen crystal structure for index, site in enumerate(crystal): # get all nearest neighbors neighbors = get_neighbors_of_site_with_index( crystal, index, approach='voronoi') # generate free integer parameters mvals = self._mvalues(l) # complex vector of all qlms generated by the free integer parameters qlms = _qlm(site, neighbors, l, mvals) # scalar product of qlm with itself dot = _scalar_product(qlms, qlms) # calculate ql see _ql method bond_order_params['q' + parameter_index] += [_ql(dot, l)] ''' Here we use a mapping from the free integer parameter set [-l,l] to the set [0, 2l] in order to index the complex vector for the computation of the wl bond order parameter It is more convenient to use the set [0, 2l] to index qlms as [0, 2l] is the set of indeces for the array elements When m1 + m2 + m3 belonging to the set [-l,l] is 0; m1 + m2 + m3 belonging to the set [0, 2l] is exactly 3 * l ''' # the set [0, 2l] iterator = np.arange(0, 2 * l + 1, 1) # initiate wli as a float wli = 0. # equivalent to a triple for loop for m1, m2, m3 in product(iterator, repeat=3): ''' This case corresponds to when m1 + m2 + m3 in the set [-l,l] is zero ''' if m1 + m2 + m3 == 3 * l: # add wli to the bond order parameter wli += _wli(qlms, l, m1, m2, m3) # normalize by the scalar product of the complex vector bond_order_params['w' + parameter_index] += [wli / (dot**(3 / 2))] # call the values in the dictionary and insert them into a list parameters = np.array(list(bond_order_params.values())) parameters[np.isnan(parameters)] = 0 self.params = parameters.T
def test_get_neighbors_of_site_with_index(self): self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0)), 4) self.assertEqual(len(get_neighbors_of_site_with_index( self.nacl, 0)), 6) self.assertEqual(len(get_neighbors_of_site_with_index( self.cscl, 0)), 8) self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0, delta=0.01)), 4) self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0, cutoff=6)), 4) self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0, approach="voronoi")), 4) self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0, approach="min_OKeeffe")), 4) self.assertEqual(len(get_neighbors_of_site_with_index( self.diamond, 0, approach="min_VIRE")), 4)
def get_analysis_and_structure(self, structure, calculate_valences=True, guesstimate_spin=False, op_threshold=0.1): """ Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). :param structure: input structure :param calculate_valences (bool): whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis :param guesstimate_spin (bool): whether to guesstimate spin state from magnetic moments or not, use with caution :param op_threshold (float): threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect :return (dict): analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer( structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(['oct', 'tet']) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Specie) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[ 0] > op_threshold: motif = 'oct' motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = 'tet' motif_order_parameter = order_params[1] else: motif = 'unknown' motif_order_parameter = None if motif == "oct" or motif == "tet": # guess spin of metal ion if guesstimate_spin and 'magmom' in site.properties: # estimate if high spin or low spin magmom = site.properties['magmom'] spin_state = self._estimate_spin_state( site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species( site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index( structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ ligand.distance(structure[idx]) for ligand in ligands ] ligands_species = list( set([str(ligand.specie) for ligand in ligands])) ligand_bond_length_spread = max(ligand_bond_lengths) - \ min(ligand_bond_lengths) def trim(f): # avoid storing to unreasonable precision, hurts readability return float("{:.4f}".format(f)) # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({ 'strength': magnitude, 'motif': motif, 'motif_order_parameter': trim(motif_order_parameter), 'spin_state': spin_state, 'species': str(site.specie), 'ligand': ligands_species[0], 'ligand_bond_lengths': [ trim(length) for length in ligand_bond_lengths ], 'ligand_bond_length_spread': trim(ligand_bond_length_spread), 'site_indices': indices }) # store reasons for not being J-T active else: non_jt_sites.append({ 'site_indices': indices, 'strength': "none", 'reason': "Not Jahn-Teller active for this " "electronic configuration." }) else: non_jt_sites.append({ 'site_indices': indices, 'strength': "none", 'reason': "motif is {}".format(motif) }) # perform aggregation of all sites if jt_sites: analysis = {'active': True} # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [ site['strength'] == "strong" for site in jt_sites ] if any(strong_magnitudes): analysis['strength'] = "strong" else: analysis['strength'] = "weak" analysis['sites'] = jt_sites return analysis, structure else: return {'active': False, 'sites': non_jt_sites}, structure
def get_analysis_and_structure(self, structure, calculate_valences=True, guesstimate_spin=False, op_threshold=0.1): """ Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). :param structure: input structure :param calculate_valences (bool): whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis :param guesstimate_spin (bool): whether to guesstimate spin state from magnetic moments or not, use with caution :param op_threshold (float): threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect :return (dict): analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer(structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(['oct', 'tet']) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Specie) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[0] > op_threshold: motif = 'oct' motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = 'tet' motif_order_parameter = order_params[1] else: motif = 'unknown' motif_order_parameter = None if motif == "oct" or motif == "tet": # guess spin of metal ion if guesstimate_spin and 'magmom' in site.properties: # estimate if high spin or low spin magmom = site.properties['magmom'] spin_state = self._estimate_spin_state(site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species(site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index(structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ligand.distance(structure[idx]) for ligand in ligands] ligands_species = list(set([str(ligand.specie) for ligand in ligands])) ligand_bond_length_spread = max(ligand_bond_lengths) - \ min(ligand_bond_lengths) def trim(f): # avoid storing to unreasonable precision, hurts readability return float("{:.4f}".format(f)) # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({'strength': magnitude, 'motif': motif, 'motif_order_parameter': trim(motif_order_parameter), 'spin_state': spin_state, 'species': str(site.specie), 'ligand': ligands_species[0], 'ligand_bond_lengths': [trim(length) for length in ligand_bond_lengths], 'ligand_bond_length_spread': trim(ligand_bond_length_spread), 'site_indices': indices}) # store reasons for not being J-T active else: non_jt_sites.append({'site_indices': indices, 'strength': "none", 'reason': "Not Jahn-Teller active for this " "electronic configuration."}) else: non_jt_sites.append({'site_indices': indices, 'strength': "none", 'reason': "motif is {}".format(motif)}) # perform aggregation of all sites if jt_sites: analysis = {'active': True} # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [site['strength'] == "strong" for site in jt_sites] if any(strong_magnitudes): analysis['strength'] = "strong" else: analysis['strength'] = "weak" analysis['sites'] = jt_sites return analysis, structure else: return {'active': False, 'sites': non_jt_sites}, structure
if thresh is None: thresh = { "qtet": 0.5, "qoct": 0.5, "qbcc": 0.5, "q6": 0.4, "qtribipyr": 0.8, "qsqpyr": 0.8 } ops = LocalStructOrderParams( ["cn", "tet", "oct", "bcc", "q6", "sq_pyr", "tri_bipyr"]) neighs_cent = get_neighbors_of_site_with_index(struct, n, approach=approach, delta=delta, cutoff=cutoff) neighs_cent.append(struct.sites[n]) opvals = ops.get_order_parameters( neighs_cent, len(neighs_cent) - 1, indices_neighs=[i for i in range(len(neighs_cent) - 1)]) cn = int(opvals[0] + 0.5) motif_type = "unrecognized" nmotif = 0 if cn == 4 and opvals[1] > thresh["qtet"]: motif_type = "tetrahedral" nmotif += 1
def get_analysis_and_structure( self, structure: Structure, calculate_valences: bool = True, guesstimate_spin: bool = False, op_threshold: float = 0.1, ) -> Tuple[Dict, Structure]: """Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). Args: structure: input structure calculate_valences: whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis (Default value = True) guesstimate_spin: whether to guesstimate spin state from magnetic moments or not, use with caution (Default value = False) op_threshold: threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect Returns: analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' (Default value = 0.1) and decorated structure """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer( structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(["oct", "tet"]) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Species) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[ 0] > op_threshold: motif = "oct" motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = "tet" motif_order_parameter = order_params[1] else: motif = "unknown" motif_order_parameter = None if motif in ["oct", "tet"]: motif = cast(Literal["oct", "tet"], motif) # mypy needs help # guess spin of metal ion if guesstimate_spin and "magmom" in site.properties: # estimate if high spin or low spin magmom = site.properties["magmom"] spin_state = self._estimate_spin_state( site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species( site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index( structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ ligand.distance(structure[idx]) for ligand in ligands ] ligands_species = list( {str(ligand.specie) for ligand in ligands}) ligand_bond_length_spread = max( ligand_bond_lengths) - min(ligand_bond_lengths) def trim(f): """ Avoid storing to unreasonable precision, hurts readability. """ return float(f"{f:.4f}") # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({ "strength": magnitude, "motif": motif, "motif_order_parameter": trim(motif_order_parameter), "spin_state": spin_state, "species": str(site.specie), "ligand": ligands_species[0], "ligand_bond_lengths": [ trim(length) for length in ligand_bond_lengths ], "ligand_bond_length_spread": trim(ligand_bond_length_spread), "site_indices": indices, }) # store reasons for not being J-T active else: non_jt_sites.append({ "site_indices": indices, "strength": "none", "reason": "Not Jahn-Teller active for this electronic configuration.", }) else: non_jt_sites.append({ "site_indices": indices, "strength": "none", "reason": f"motif is {motif}", }) # perform aggregation of all sites if jt_sites: analysis = {"active": True} # type: Dict[str, Any] # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [ site["strength"] == "strong" for site in jt_sites ] if any(strong_magnitudes): analysis["strength"] = "strong" else: analysis["strength"] = "weak" analysis["sites"] = jt_sites return analysis, structure return {"active": False, "sites": non_jt_sites}, structure
def site_is_of_motif_type(struct, n, neighbors_list=None, approach="min_dist", delta=0.1, cutoff=10.0, thresh=None): """ Returns the motif type of the site with index n in structure struct; currently featuring "tetrahedral", "octahedral", "bcc", and "cp" (close-packed: fcc and hcp) as well as "square pyramidal" and "trigonal bipyramidal". If the site is not recognized, "unrecognized" is returned. If a site should be assigned to two different motifs, "multiple assignments" is returned. Args: struct (Structure): input structure. n (int): index of site in Structure object for which motif type is to be determined. approach (str): type of neighbor-finding approach, where "min_dist" will use the MinimumDistanceNN class, "voronoi" the VoronoiNN class, "min_OKeeffe" the MinimumOKeeffe class, and "min_VIRE" the MinimumVIRENN class. delta (float): tolerance involved in neighbor finding. cutoff (float): (large) radius to find tentative neighbors. thresh (dict): thresholds for motif criteria (currently, required keys and their default values are "qtet": 0.5, "qoct": 0.5, "qbcc": 0.5, "q6": 0.4). Returns: motif type (str). """ # | - site_is_of_motif_type # print("THIS IS MY CUSTOM METHOD, NOT NATIVE PYMATGEN") if thresh is None: thresh = { "qtet": 0.5, "qoct": 0.5, "qbcc": 0.5, "q6": 0.4, "qtribipyr": 0.8, "qsqpyr": 0.8 } ops = LocalStructOrderParams( ["cn", "tet", "oct", "bcc", "q6", "sq_pyr", "tri_bipyr"]) # ######################################################################### if neighbors_list is None: neighs_cent = get_neighbors_of_site_with_index(struct, n, approach=approach, delta=delta, cutoff=cutoff) else: neighs_cent = neighbors_list neighs_cent.append(struct.sites[n]) opvals = ops.get_order_parameters( neighs_cent, len(neighs_cent) - 1, indices_neighs=[i for i in range(len(neighs_cent) - 1)]) cn = int(opvals[0] + 0.5) motif_type = "unrecognized" nmotif = 0 if cn == 4 and opvals[1] > thresh["qtet"]: motif_type = "tetrahedral" nmotif += 1 if cn == 5 and opvals[5] > thresh["qsqpyr"]: motif_type = "square pyramidal" nmotif += 1 if cn == 5 and opvals[6] > thresh["qtribipyr"]: motif_type = "trigonal bipyramidal" nmotif += 1 if cn == 6 and opvals[2] > thresh["qoct"]: motif_type = "octahedral" nmotif += 1 if cn == 8 and (opvals[3] > thresh["qbcc"] and opvals[1] < thresh["qtet"]): motif_type = "bcc" nmotif += 1 if cn == 12 and (opvals[4] > thresh["q6"] and opvals[1] < thresh["q6"] and opvals[2] < thresh["q6"] and opvals[3] < thresh["q6"]): motif_type = "cp" nmotif += 1 if nmotif > 1: motif_type = "multiple assignments" return motif_type