def tag_structure( self, structure: Structure, calculate_valences: bool = True, guesstimate_spin: bool = False, op_threshold: float = 0.1, ) -> Structure: """ Convenience method, uses get_analysis_and_structure method. Add a "possible_jt_active" site property on Structure. 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: Decorated Structure, will be in primitive setting. """ try: analysis, structure = self.get_analysis_and_structure( structure, calculate_valences=calculate_valences, guesstimate_spin=guesstimate_spin, op_threshold=op_threshold, ) jt_sites = [False] * len(structure) if analysis["active"]: for site in analysis["sites"]: for index in site["site_indices"]: jt_sites[index] = True structure.add_site_property("possible_jt_active", jt_sites) return structure except Exception as e: warnings.warn( "Error analyzing {}: {}".format( structure.composition.reduced_formula, e ) ) return structure
def analyze(topology: Structure, skin: float = 5e-3) -> List[Molecule]: # containers for the output fragments = [] # we iterate over non-dummies dmat = topology.distance_matrix dummies = numpy.array(topology.indices_from_symbol("X")) not_dummies = numpy.array( [i for i in range(len(topology)) if i not in dummies]) # initialize and set tags tags = numpy.zeros(len(topology), dtype=int) tags[dummies] = dummies + 1 topology.add_site_property(property_name="tags", values=tags) # TODO : site properties # get the distances between centers and connections distances = dmat[not_dummies][:, dummies] coordinations = numpy.array(topology.atomic_numbers)[not_dummies] partitions = numpy.argsort(distances, axis=1) for center_idx, best_dummies in enumerate(partitions): coordination = coordinations[center_idx] if coordination < len(best_dummies): best_dummies = best_dummies[:coordination] cutoff = distances[center_idx][best_dummies].max() + skin # now extract the corresponding fragment fragment_center = topology.sites[not_dummies[center_idx]] fragment_sites = topology.get_neighbors(fragment_center, r=cutoff) # some topologies in the RCSR have a crazy size # we ignore them with the heat of a thousand suns assert len( fragment_sites) <= 12, "Fragment size larger than limit of 12." # store as molecule to use the point group analysis fragment = Molecule.from_sites( fragment_sites) #, charge=1, spin_multiplicity=1) # this is needed because X have no mass, leading to a # symmetrization error. fragment.replace_species({"X": "He"}) pg = PointGroupAnalyzer(fragment, tolerance=0.1) # from pymatgen.io.ase import AseAtomsAdaptor # view(AseAtomsAdaptor.get_atoms(fragment)) if pg.sch_symbol == "C1": raise ("NoSymm") fragment.replace_species({"He": "X"}) fragments.append(Fragment(atoms=fragment, symmetry=pg, name="slot")) return fragments
class StructureTest(PymatgenTest): def setUp(self): coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], coords) def test_mutable_sequence_methods(self): s = self.structure s[0] = "Fe" self.assertEqual(s.formula, "Fe1 Si1") s[0] = "Fe", [0.5, 0.5, 0.5] self.assertEqual(s.formula, "Fe1 Si1") self.assertArrayAlmostEqual(s[0].frac_coords, [0.5, 0.5, 0.5]) s.reverse() self.assertEqual(s[0].specie, Element("Si")) self.assertArrayAlmostEqual(s[0].frac_coords, [0.75, 0.5, 0.75]) s[0] = {"Mn": 0.5} self.assertEqual(s.formula, "Mn0.5 Fe1") del s[1] self.assertEqual(s.formula, "Mn0.5") s[0] = "Fe", [0.9, 0.9, 0.9], {"magmom": 5} self.assertEqual(s.formula, "Fe1") self.assertEqual(s[0].magmom, 5) def test_non_hash(self): self.assertRaises(TypeError, dict, [(self.structure, 1)]) def test_sort(self): s = self.structure s[0] = "F" s.sort() self.assertEqual(s[0].species_string, "Si") self.assertEqual(s[1].species_string, "F") s.sort(key=lambda site: site.species_string) self.assertEqual(s[0].species_string, "F") self.assertEqual(s[1].species_string, "Si") s.sort(key=lambda site: site.species_string, reverse=True) self.assertEqual(s[0].species_string, "Si") self.assertEqual(s[1].species_string, "F") def test_append_insert_remove_replace(self): s = self.structure s.insert(1, "O", [0.5, 0.5, 0.5]) self.assertEqual(s.formula, "Si2 O1") self.assertTrue(s.ntypesp == 2) self.assertTrue(s.symbol_set == ("Si", "O")) self.assertTrue(s.indices_from_symbol("Si") == (0,2)) self.assertTrue(s.indices_from_symbol("O") == (1,)) del s[2] self.assertEqual(s.formula, "Si1 O1") self.assertTrue(s.indices_from_symbol("Si") == (0,)) self.assertTrue(s.indices_from_symbol("O") == (1,)) s.append("N", [0.25, 0.25, 0.25]) self.assertEqual(s.formula, "Si1 N1 O1") self.assertTrue(s.ntypesp == 3) self.assertTrue(s.symbol_set == ("Si", "O", "N")) self.assertTrue(s.indices_from_symbol("Si") == (0,)) self.assertTrue(s.indices_from_symbol("O") == (1,)) self.assertTrue(s.indices_from_symbol("N") == (2,)) s[0] = "Ge" self.assertEqual(s.formula, "Ge1 N1 O1") self.assertTrue(s.symbol_set == ("Ge", "O", "N")) s.replace_species({"Ge": "Si"}) self.assertEqual(s.formula, "Si1 N1 O1") self.assertTrue(s.ntypesp == 3) s.replace_species({"Si": {"Ge": 0.5, "Si": 0.5}}) self.assertEqual(s.formula, "Si0.5 Ge0.5 N1 O1") #this should change the .5Si .5Ge sites to .75Si .25Ge s.replace_species({"Ge": {"Ge": 0.5, "Si": 0.5}}) self.assertEqual(s.formula, "Si0.75 Ge0.25 N1 O1") # In this case, s.ntypesp is ambiguous. # for the time being, we raise AttributeError. with self.assertRaises(AttributeError): s.ntypesp s.remove_species(["Si"]) self.assertEqual(s.formula, "Ge0.25 N1 O1") s.remove_sites([1, 2]) self.assertEqual(s.formula, "Ge0.25") def test_add_site_property(self): s = self.structure s.add_site_property("charge", [4.1, -5]) self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[1].charge, -5) s.add_site_property("magmom", [3, 2]) self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[0].magmom, 3) def test_propertied_structure(self): #Make sure that site properties are set to None for missing values. s = self.structure s.add_site_property("charge", [4.1, -5]) s.append("Li", [0.3, 0.3 ,0.3]) self.assertEqual(len(s.site_properties["charge"]), 3) def test_perturb(self): d = 0.1 pre_perturbation_sites = self.structure.sites[:] self.structure.perturb(distance=d) post_perturbation_sites = self.structure.sites for i, x in enumerate(pre_perturbation_sites): self.assertAlmostEqual(x.distance(post_perturbation_sites[i]), d, 3, "Bad perturbation distance") def test_add_oxidation_states(self): oxidation_states = {"Si": -4} self.structure.add_oxidation_state_by_element(oxidation_states) for site in self.structure: for k in site.species_and_occu.keys(): self.assertEqual(k.oxi_state, oxidation_states[k.symbol], "Wrong oxidation state assigned!") oxidation_states = {"Fe": 2} self.assertRaises(ValueError, self.structure.add_oxidation_state_by_element, oxidation_states) self.structure.add_oxidation_state_by_site([2, -4]) self.assertEqual(self.structure[0].specie.oxi_state, 2) self.assertRaises(ValueError, self.structure.add_oxidation_state_by_site, [1]) def test_remove_oxidation_states(self): co_elem = Element("Co") o_elem = Element("O") co_specie = Specie("Co", 2) o_specie = Specie("O", -2) coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice.cubic(10) s_elem = Structure(lattice, [co_elem, o_elem], coords) s_specie = Structure(lattice, [co_specie, o_specie], coords) s_specie.remove_oxidation_states() self.assertEqual(s_elem, s_specie, "Oxidation state remover " "failed") def test_apply_operation(self): op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 90) s = self.structure.copy() s.apply_operation(op) self.assertArrayAlmostEqual( s.lattice.matrix, [[0.000000, 3.840198, 0.000000], [-3.325710, 1.920099, 0.000000], [2.217138, -0.000000, 3.135509]], 5) op = SymmOp([[1, 1, 0, 0.5], [1, 0, 0, 0.5], [0, 0, 1, 0.5], [0, 0, 0, 1]]) s = self.structure.copy() s.apply_operation(op, fractional=True) self.assertArrayAlmostEqual( s.lattice.matrix, [[5.760297, 3.325710, 0.000000], [3.840198, 0.000000, 0.000000], [0.000000, -2.217138, 3.135509]], 5) def test_apply_strain(self): s = self.structure initial_coord = s[1].coords s.apply_strain(0.01) self.assertAlmostEqual( s.lattice.abc, (3.8785999130369997, 3.878600984287687, 3.8785999130549516)) self.assertArrayAlmostEqual(s[1].coords, initial_coord * 1.01) a1, b1, c1 = s.lattice.abc s.apply_strain([0.1, 0.2, 0.3]) a2, b2, c2 = s.lattice.abc self.assertAlmostEqual(a2 / a1, 1.1) self.assertAlmostEqual(b2 / b1, 1.2) self.assertAlmostEqual(c2 / c1, 1.3) def test_scale_lattice(self): initial_coord = self.structure[1].coords self.structure.scale_lattice(self.structure.volume * 1.01 ** 3) self.assertArrayAlmostEqual( self.structure.lattice.abc, (3.8785999130369997, 3.878600984287687, 3.8785999130549516)) self.assertArrayAlmostEqual(self.structure[1].coords, initial_coord * 1.01) def test_translate_sites(self): self.structure.translate_sites([0, 1], [0.5, 0.5, 0.5], frac_coords=True) self.assertArrayEqual(self.structure.frac_coords[0], [0.5, 0.5, 0.5]) self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=False) self.assertArrayAlmostEqual(self.structure.cart_coords[0], [3.38014845, 1.05428585, 2.06775453]) self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=True, to_unit_cell=False) self.assertArrayAlmostEqual(self.structure.frac_coords[0], [1.00187517, 1.25665291, 1.15946374]) def test_mul(self): self.structure *= [2, 1, 1] self.assertEqual(self.structure.formula, "Si4") s = [2, 1, 1] * self.structure self.assertEqual(s.formula, "Si8") self.assertIsInstance(s, Structure) s = self.structure * [[1, 0, 0], [2, 1, 0], [0, 0, 2]] self.assertEqual(s.formula, "Si8") self.assertArrayAlmostEqual(s.lattice.abc, [7.6803959, 17.5979979, 7.6803959]) def test_make_supercell(self): self.structure.make_supercell([2, 1, 1]) self.assertEqual(self.structure.formula, "Si4") self.structure.make_supercell([[1, 0, 0], [2, 1, 0], [0, 0, 1]]) self.assertEqual(self.structure.formula, "Si4") self.structure.make_supercell(2) self.assertEqual(self.structure.formula, "Si32") self.assertArrayAlmostEqual(self.structure.lattice.abc, [15.360792, 35.195996, 7.680396], 5) def test_disordered_supercell_primitive_cell(self): l = Lattice.cubic(2) f = [[0.5, 0.5, 0.5]] sp = [{'Si': 0.54738}] s = Structure(l, sp, f) #this supercell often breaks things s.make_supercell([[0,-1,1],[-1,1,0],[1,1,1]]) self.assertEqual(len(s.get_primitive_structure()), 1) def test_another_supercell(self): #this is included b/c for some reason the old algo was failing on it s = self.structure.copy() s.make_supercell([[0, 2, 2], [2, 0, 2], [2, 2, 0]]) self.assertEqual(s.formula, "Si32") s = self.structure.copy() s.make_supercell([[0, 2, 0], [1, 0, 0], [0, 0, 1]]) self.assertEqual(s.formula, "Si4") def test_to_from_dict(self): d = self.structure.as_dict() s2 = Structure.from_dict(d) self.assertEqual(type(s2), Structure) def test_to_from_file_string(self): for fmt in ["cif", "json", "poscar", "cssr", "yaml", "xsf"]: s = self.structure.to(fmt=fmt) self.assertIsNotNone(s) ss = Structure.from_str(s, fmt=fmt) self.assertArrayAlmostEqual( ss.lattice.lengths_and_angles, self.structure.lattice.lengths_and_angles, decimal=5) self.assertArrayAlmostEqual(ss.frac_coords, self.structure.frac_coords) self.assertIsInstance(ss, Structure) self.structure.to(filename="POSCAR.testing") self.assertTrue(os.path.exists("POSCAR.testing")) os.remove("POSCAR.testing") self.structure.to(filename="structure_testing.json") self.assertTrue(os.path.exists("structure_testing.json")) s = Structure.from_file("structure_testing.json") self.assertEqual(s, self.structure) os.remove("structure_testing.json") def test_from_spacegroup(self): s1 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]]) self.assertEqual(s1.formula, "Li8 O4") s2 = Structure.from_spacegroup(225, Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]]) self.assertEqual(s1, s2) s2 = Structure.from_spacegroup(225, Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]], site_properties={"charge": [1, -2]}) self.assertEqual(sum(s2.site_properties["charge"]), 0) s = Structure.from_spacegroup("Pm-3m", Lattice.cubic(3), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]]) self.assertEqual(s.formula, "Cs1 Cl1") self.assertRaises(ValueError, Structure.from_spacegroup, "Pm-3m", Lattice.tetragonal(1, 3), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]]) def test_merge_sites(self): species = [{'Ag': 0.5}, {'Cl': 0.25}, {'Cl': 0.1}, {'Ag': 0.5}, {'F': 0.15}, {'F': 0.1}] coords = [[0, 0, 0], [0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0, 0, 0], [0.5, 0.5, 1.501], [0.5, 0.5, 1.501]] s = Structure(Lattice.cubic(1), species, coords) s.merge_sites() self.assertEqual(s[0].specie.symbol, 'Ag') self.assertEqual(s[1].species_and_occu, Composition({'Cl': 0.35, 'F': 0.25})) self.assertArrayAlmostEqual(s[1].frac_coords, [.5, .5, .5005]) def test_properties(self): self.assertEqual(self.structure.num_sites, len(self.structure)) self.structure.make_supercell(2) self.structure[1] = "C" sites = list(self.structure.group_by_types()) self.assertEqual(sites[-1].specie.symbol, "C") self.structure.add_oxidation_state_by_element({"Si": 4, "C": 2}) self.assertEqual(self.structure.charge, 62) def test_init_error(self): self.assertRaises(StructureError, Structure, Lattice.cubic(3), ["Si"], [[0, 0, 0], [0.5, 0.5, 0.5]]) def test_from_sites(self): self.structure.add_site_property("hello", [1, 2]) s = Structure.from_sites(self.structure, to_unit_cell=True) self.assertEqual(s.site_properties["hello"][1], 2) def test_magic(self): s = Structure.from_sites(self.structure) self.assertEqual(s, self.structure) self.assertNotEqual(s, None) s.apply_strain(0.5) self.assertNotEqual(s, self.structure) self.assertNotEqual(self.structure * 2, self.structure)
def __init__( self, structure: Structure, overwrite_magmom_mode: Union[OverwriteMagmomMode, str] = "none", round_magmoms: bool = False, detect_valences: bool = False, make_primitive: bool = True, default_magmoms: bool = None, set_net_positive: bool = True, threshold: float = 0.1, ): """ A class which provides a few helpful methods to analyze collinear magnetic structures. If magnetic moments are not defined, moments will be taken either from default_magmoms.yaml (similar to the default magmoms in MPRelaxSet, with a few extra definitions) or from a specie:magmom dict provided by the default_magmoms kwarg. Input magmoms can be replaced using the 'overwrite_magmom_mode' kwarg. This can be: * "none" to do nothing, * "respect_sign" which will overwrite existing magmoms with those from default_magmoms but will keep sites with positive magmoms positive, negative magmoms negative and zero magmoms zero, * "respect_zeros", which will give a ferromagnetic structure (all positive magmoms from default_magmoms) but still keep sites with zero magmoms as zero, * "replace_all" which will try to guess initial magmoms for all sites in the structure irrespective of input structure (this is most suitable for an initial DFT calculation), * "replace_all_if_undefined" is the same as "replace_all" but only if no magmoms are defined in input structure, otherwise it will respect existing magmoms. * "normalize" will normalize magmoms to unity, but will respect sign (used for comparing orderings), magmoms < theshold will be set to zero :param structure: Structure object :param overwrite_magmom_mode (str): default "none" :param round_magmoms (int or bool): will round input magmoms to specified number of decimal places if integer is supplied, if set to a float will try and group magmoms together using a kernel density estimator of provided width, and extracting peaks of the estimator :param detect_valences (bool): if True, will attempt to assign valences to input structure :param make_primitive (bool): if True, will transform to primitive magnetic cell :param default_magmoms (dict): (optional) dict specifying default magmoms :param set_net_positive (bool): if True, will change sign of magnetic moments such that the net magnetization is positive. Argument will be ignored if mode "respect_sign" is used. :param threshold (float): number (in Bohr magnetons) below which magmoms will be rounded to zero, default of 0.1 can probably be increased for many magnetic systems, depending on your application """ if default_magmoms: self.default_magmoms = default_magmoms else: self.default_magmoms = DEFAULT_MAGMOMS structure = structure.copy() # check for disorder if not structure.is_ordered: raise NotImplementedError( "Not implemented for disordered structures, " "make ordered approximation first.") if detect_valences: trans = AutoOxiStateDecorationTransformation() bva = BVAnalyzer() try: structure = trans.apply_transformation(structure) except ValueError: warnings.warn("Could not assign valences " "for {}".format( structure.composition.reduced_formula)) # check to see if structure has magnetic moments # on site properties or species spin properties, # prioritize site properties has_magmoms = bool(structure.site_properties.get("magmom", False)) has_spin = False for comp in structure.species_and_occu: for sp, occu in comp.items(): if getattr(sp, "spin", False): has_spin = True # perform input sanitation ... # rest of class will assume magnetic moments # are stored on site properties: # this is somewhat arbitrary, arguments can # be made for both approaches if has_magmoms and has_spin: raise ValueError("Structure contains magnetic moments on both " "magmom site properties and spin species " "properties. This is ambiguous. Remove one or " "the other.") elif has_magmoms: if None in structure.site_properties["magmom"]: warnings.warn("Be careful with mixing types in your magmom " "site properties. Any 'None' magmoms have been " "replaced with zero.") magmoms = [ m if m else 0 for m in structure.site_properties["magmom"] ] elif has_spin: magmoms = [getattr(sp, "spin", 0) for sp in structure.species] structure.remove_spin() else: # no magmoms present, add zero magmoms for now magmoms = [0] * len(structure) # and overwrite magmoms with default magmoms later unless otherwise stated if overwrite_magmom_mode == "replace_all_if_undefined": overwrite_magmom_mode = "replace_all" # test to see if input structure has collinear magmoms self.is_collinear = Magmom.are_collinear(magmoms) if not self.is_collinear: warnings.warn( "This class is not designed to be used with " "non-collinear structures. If your structure is " "only slightly non-collinear (e.g. canted) may still " "give useful results, but use with caution.") # this is for collinear structures only, make sure magmoms # are all floats magmoms = list(map(float, magmoms)) # set properties that should be done /before/ we process input magmoms self.total_magmoms = sum(magmoms) self.magnetization = sum(magmoms) / structure.volume # round magmoms below threshold to zero magmoms = [m if abs(m) > threshold else 0 for m in magmoms] # overwrite existing magmoms with default_magmoms if overwrite_magmom_mode not in ( "none", "respect_sign", "respect_zeros", "replace_all", "replace_all_if_undefined", "normalize", ): raise ValueError("Unsupported mode.") for idx, site in enumerate(structure): if site.species_string in self.default_magmoms: # look for species first, e.g. Fe2+ default_magmom = self.default_magmoms[site.species_string] elif (isinstance(site.specie, Specie) and str(site.specie.element) in self.default_magmoms): # look for element, e.g. Fe default_magmom = self.default_magmoms[str(site.specie.element)] else: default_magmom = 0 # overwrite_magmom_mode = "respect_sign" will change magnitude of # existing moments only, and keep zero magmoms as # zero: it will keep the magnetic ordering intact if overwrite_magmom_mode == "respect_sign": set_net_positive = False if magmoms[idx] > 0: magmoms[idx] = default_magmom elif magmoms[idx] < 0: magmoms[idx] = -default_magmom # overwrite_magmom_mode = "respect_zeros" will give a ferromagnetic # structure but will keep zero magmoms as zero elif overwrite_magmom_mode == "respect_zeros": if magmoms[idx] != 0: magmoms[idx] = default_magmom # overwrite_magmom_mode = "replace_all" will ignore input magmoms # and give a ferromagnetic structure with magnetic # moments on *all* atoms it thinks could be magnetic elif overwrite_magmom_mode == "replace_all": magmoms[idx] = default_magmom # overwrite_magmom_mode = "normalize" set magmoms magnitude to 1 elif overwrite_magmom_mode == "normalize": if magmoms[idx] != 0: magmoms[idx] = int(magmoms[idx] / abs(magmoms[idx])) # round magmoms, used to smooth out computational data magmoms = (self._round_magmoms(magmoms, round_magmoms) if round_magmoms else magmoms) if set_net_positive: sign = np.sum(magmoms) if sign < 0: magmoms = -np.array(magmoms) structure.add_site_property("magmom", magmoms) if make_primitive: structure = structure.get_primitive_structure(use_site_props=True) self.structure = structure
class StructureTest(PymatgenTest): def setUp(self): coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice([[3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603]]) self.structure = Structure(lattice, ["Si", "Si"], coords) def test_mutable_sequence_methods(self): s = self.structure s[0] = "Fe" self.assertEqual(s.formula, "Fe1 Si1") s[0] = "Fe", [0.5, 0.5, 0.5] self.assertEqual(s.formula, "Fe1 Si1") self.assertArrayAlmostEqual(s[0].frac_coords, [0.5, 0.5, 0.5]) s.reverse() self.assertEqual(s[0].specie, Element("Si")) self.assertArrayAlmostEqual(s[0].frac_coords, [0.75, 0.5, 0.75]) s[0] = {"Mn": 0.5} self.assertEqual(s.formula, "Mn0.5 Fe1") del s[1] self.assertEqual(s.formula, "Mn0.5") s[0] = "Fe", [0.9, 0.9, 0.9], {"magmom": 5} self.assertEqual(s.formula, "Fe1") self.assertEqual(s[0].magmom, 5) # Test atomic replacement. s["Fe"] = "Mn" self.assertEqual(s.formula, "Mn1") # Test slice replacement. s = PymatgenTest.get_structure("Li2O") s[1:3] = "S" self.assertEqual(s.formula, "Li1 S2") def test_non_hash(self): self.assertRaises(TypeError, dict, [(self.structure, 1)]) def test_sort(self): s = self.structure s[0] = "F" s.sort() self.assertEqual(s[0].species_string, "Si") self.assertEqual(s[1].species_string, "F") s.sort(key=lambda site: site.species_string) self.assertEqual(s[0].species_string, "F") self.assertEqual(s[1].species_string, "Si") s.sort(key=lambda site: site.species_string, reverse=True) self.assertEqual(s[0].species_string, "Si") self.assertEqual(s[1].species_string, "F") def test_append_insert_remove_replace(self): s = self.structure s.insert(1, "O", [0.5, 0.5, 0.5]) self.assertEqual(s.formula, "Si2 O1") self.assertTrue(s.ntypesp == 2) self.assertTrue(s.symbol_set == ("Si", "O")) self.assertTrue(s.indices_from_symbol("Si") == (0, 2)) self.assertTrue(s.indices_from_symbol("O") == (1, )) del s[2] self.assertEqual(s.formula, "Si1 O1") self.assertTrue(s.indices_from_symbol("Si") == (0, )) self.assertTrue(s.indices_from_symbol("O") == (1, )) s.append("N", [0.25, 0.25, 0.25]) self.assertEqual(s.formula, "Si1 N1 O1") self.assertTrue(s.ntypesp == 3) self.assertTrue(s.symbol_set == ("Si", "O", "N")) self.assertTrue(s.indices_from_symbol("Si") == (0, )) self.assertTrue(s.indices_from_symbol("O") == (1, )) self.assertTrue(s.indices_from_symbol("N") == (2, )) s[0] = "Ge" self.assertEqual(s.formula, "Ge1 N1 O1") self.assertTrue(s.symbol_set == ("Ge", "O", "N")) s.replace_species({"Ge": "Si"}) self.assertEqual(s.formula, "Si1 N1 O1") self.assertTrue(s.ntypesp == 3) s.replace_species({"Si": {"Ge": 0.5, "Si": 0.5}}) self.assertEqual(s.formula, "Si0.5 Ge0.5 N1 O1") #this should change the .5Si .5Ge sites to .75Si .25Ge s.replace_species({"Ge": {"Ge": 0.5, "Si": 0.5}}) self.assertEqual(s.formula, "Si0.75 Ge0.25 N1 O1") # In this case, s.ntypesp is ambiguous. # for the time being, we raise AttributeError. with self.assertRaises(AttributeError): s.ntypesp s.remove_species(["Si"]) self.assertEqual(s.formula, "Ge0.25 N1 O1") s.remove_sites([1, 2]) self.assertEqual(s.formula, "Ge0.25") def test_add_site_property(self): s = self.structure s.add_site_property("charge", [4.1, -5]) self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[1].charge, -5) s.add_site_property("magmom", [3, 2]) self.assertEqual(s[0].charge, 4.1) self.assertEqual(s[0].magmom, 3) def test_propertied_structure(self): #Make sure that site properties are set to None for missing values. s = self.structure s.add_site_property("charge", [4.1, -5]) s.append("Li", [0.3, 0.3, 0.3]) self.assertEqual(len(s.site_properties["charge"]), 3) def test_perturb(self): d = 0.1 pre_perturbation_sites = self.structure.sites[:] self.structure.perturb(distance=d) post_perturbation_sites = self.structure.sites for i, x in enumerate(pre_perturbation_sites): self.assertAlmostEqual(x.distance(post_perturbation_sites[i]), d, 3, "Bad perturbation distance") def test_add_oxidation_states(self): oxidation_states = {"Si": -4} self.structure.add_oxidation_state_by_element(oxidation_states) for site in self.structure: for k in site.species_and_occu.keys(): self.assertEqual(k.oxi_state, oxidation_states[k.symbol], "Wrong oxidation state assigned!") oxidation_states = {"Fe": 2} self.assertRaises(ValueError, self.structure.add_oxidation_state_by_element, oxidation_states) self.structure.add_oxidation_state_by_site([2, -4]) self.assertEqual(self.structure[0].specie.oxi_state, 2) self.assertRaises(ValueError, self.structure.add_oxidation_state_by_site, [1]) def test_remove_oxidation_states(self): co_elem = Element("Co") o_elem = Element("O") co_specie = Specie("Co", 2) o_specie = Specie("O", -2) coords = list() coords.append([0, 0, 0]) coords.append([0.75, 0.5, 0.75]) lattice = Lattice.cubic(10) s_elem = Structure(lattice, [co_elem, o_elem], coords) s_specie = Structure(lattice, [co_specie, o_specie], coords) s_specie.remove_oxidation_states() self.assertEqual(s_elem, s_specie, "Oxidation state remover " "failed") def test_apply_operation(self): op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 90) s = self.structure.copy() s.apply_operation(op) self.assertArrayAlmostEqual( s.lattice.matrix, [[0.000000, 3.840198, 0.000000], [-3.325710, 1.920099, 0.000000], [2.217138, -0.000000, 3.135509]], 5) op = SymmOp([[1, 1, 0, 0.5], [1, 0, 0, 0.5], [0, 0, 1, 0.5], [0, 0, 0, 1]]) s = self.structure.copy() s.apply_operation(op, fractional=True) self.assertArrayAlmostEqual( s.lattice.matrix, [[5.760297, 3.325710, 0.000000], [3.840198, 0.000000, 0.000000], [0.000000, -2.217138, 3.135509]], 5) def test_apply_strain(self): s = self.structure initial_coord = s[1].coords s.apply_strain(0.01) self.assertAlmostEqual( s.lattice.abc, (3.8785999130369997, 3.878600984287687, 3.8785999130549516)) self.assertArrayAlmostEqual(s[1].coords, initial_coord * 1.01) a1, b1, c1 = s.lattice.abc s.apply_strain([0.1, 0.2, 0.3]) a2, b2, c2 = s.lattice.abc self.assertAlmostEqual(a2 / a1, 1.1) self.assertAlmostEqual(b2 / b1, 1.2) self.assertAlmostEqual(c2 / c1, 1.3) def test_scale_lattice(self): initial_coord = self.structure[1].coords self.structure.scale_lattice(self.structure.volume * 1.01**3) self.assertArrayAlmostEqual( self.structure.lattice.abc, (3.8785999130369997, 3.878600984287687, 3.8785999130549516)) self.assertArrayAlmostEqual(self.structure[1].coords, initial_coord * 1.01) def test_translate_sites(self): self.structure.translate_sites([0, 1], [0.5, 0.5, 0.5], frac_coords=True) self.assertArrayEqual(self.structure.frac_coords[0], [0.5, 0.5, 0.5]) self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=False) self.assertArrayAlmostEqual(self.structure.cart_coords[0], [3.38014845, 1.05428585, 2.06775453]) self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=True, to_unit_cell=False) self.assertArrayAlmostEqual(self.structure.frac_coords[0], [1.00187517, 1.25665291, 1.15946374]) def test_mul(self): self.structure *= [2, 1, 1] self.assertEqual(self.structure.formula, "Si4") s = [2, 1, 1] * self.structure self.assertEqual(s.formula, "Si8") self.assertIsInstance(s, Structure) s = self.structure * [[1, 0, 0], [2, 1, 0], [0, 0, 2]] self.assertEqual(s.formula, "Si8") self.assertArrayAlmostEqual(s.lattice.abc, [7.6803959, 17.5979979, 7.6803959]) def test_make_supercell(self): self.structure.make_supercell([2, 1, 1]) self.assertEqual(self.structure.formula, "Si4") self.structure.make_supercell([[1, 0, 0], [2, 1, 0], [0, 0, 1]]) self.assertEqual(self.structure.formula, "Si4") self.structure.make_supercell(2) self.assertEqual(self.structure.formula, "Si32") self.assertArrayAlmostEqual(self.structure.lattice.abc, [15.360792, 35.195996, 7.680396], 5) def test_disordered_supercell_primitive_cell(self): l = Lattice.cubic(2) f = [[0.5, 0.5, 0.5]] sp = [{'Si': 0.54738}] s = Structure(l, sp, f) #this supercell often breaks things s.make_supercell([[0, -1, 1], [-1, 1, 0], [1, 1, 1]]) self.assertEqual(len(s.get_primitive_structure()), 1) def test_another_supercell(self): #this is included b/c for some reason the old algo was failing on it s = self.structure.copy() s.make_supercell([[0, 2, 2], [2, 0, 2], [2, 2, 0]]) self.assertEqual(s.formula, "Si32") s = self.structure.copy() s.make_supercell([[0, 2, 0], [1, 0, 0], [0, 0, 1]]) self.assertEqual(s.formula, "Si4") def test_to_from_dict(self): d = self.structure.as_dict() s2 = Structure.from_dict(d) self.assertEqual(type(s2), Structure) def test_to_from_abivars(self): """Test as_dict, from_dict with fmt == abivars.""" d = self.structure.as_dict(fmt="abivars") s2 = Structure.from_dict(d, fmt="abivars") self.assertEqual(s2, self.structure) self.assertEqual(type(s2), Structure) def test_to_from_file_string(self): for fmt in ["cif", "json", "poscar", "cssr", "yaml", "xsf"]: s = self.structure.to(fmt=fmt) self.assertIsNotNone(s) ss = Structure.from_str(s, fmt=fmt) self.assertArrayAlmostEqual( ss.lattice.lengths_and_angles, self.structure.lattice.lengths_and_angles, decimal=5) self.assertArrayAlmostEqual(ss.frac_coords, self.structure.frac_coords) self.assertIsInstance(ss, Structure) self.structure.to(filename="POSCAR.testing") self.assertTrue(os.path.exists("POSCAR.testing")) os.remove("POSCAR.testing") self.structure.to(filename="structure_testing.json") self.assertTrue(os.path.exists("structure_testing.json")) s = Structure.from_file("structure_testing.json") self.assertEqual(s, self.structure) os.remove("structure_testing.json") def test_from_spacegroup(self): s1 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]]) self.assertEqual(s1.formula, "Li8 O4") s2 = Structure.from_spacegroup(225, Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]]) self.assertEqual(s1, s2) s2 = Structure.from_spacegroup(225, Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]], site_properties={"charge": [1, -2]}) self.assertEqual(sum(s2.site_properties["charge"]), 0) s = Structure.from_spacegroup("Pm-3m", Lattice.cubic(3), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]]) self.assertEqual(s.formula, "Cs1 Cl1") self.assertRaises(ValueError, Structure.from_spacegroup, "Pm-3m", Lattice.tetragonal(1, 3), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]]) self.assertRaises(ValueError, Structure.from_spacegroup, "Pm-3m", Lattice.cubic(3), ["Cs"], [[0, 0, 0], [0.5, 0.5, 0.5]]) def test_from_magnetic_spacegroup(self): # AFM MnF s1 = Structure.from_magnetic_spacegroup( "P4_2'/mnm'", Lattice.tetragonal(4.87, 3.30), ["Mn", "F"], [[0, 0, 0], [0.30, 0.30, 0.00]], {'magmom': [4, 0]}) self.assertEqual(s1.formula, "Mn2 F4") self.assertEqual(sum(map(float, s1.site_properties['magmom'])), 0) self.assertEqual(max(map(float, s1.site_properties['magmom'])), 4) self.assertEqual(min(map(float, s1.site_properties['magmom'])), -4) # AFM LaMnO3, ordered on (001) planes s2 = Structure.from_magnetic_spacegroup( "Pn'ma'", Lattice.orthorhombic(5.75, 7.66, 5.53), ["La", "Mn", "O", "O"], [[0.05, 0.25, 0.99], [0.00, 0.00, 0.50], [0.48, 0.25, 0.08], [0.31, 0.04, 0.72]], {'magmom': [0, Magmom([4, 0, 0]), 0, 0]}) self.assertEqual(s2.formula, "La4 Mn4 O12") self.assertEqual(sum(map(float, s2.site_properties['magmom'])), 0) self.assertEqual(max(map(float, s2.site_properties['magmom'])), 4) self.assertEqual(min(map(float, s2.site_properties['magmom'])), -4) def test_merge_sites(self): species = [{ 'Ag': 0.5 }, { 'Cl': 0.25 }, { 'Cl': 0.1 }, { 'Ag': 0.5 }, { 'F': 0.15 }, { 'F': 0.1 }] coords = [[0, 0, 0], [0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0, 0, 0], [0.5, 0.5, 1.501], [0.5, 0.5, 1.501]] s = Structure(Lattice.cubic(1), species, coords) s.merge_sites(mode="s") self.assertEqual(s[0].specie.symbol, 'Ag') self.assertEqual(s[1].species_and_occu, Composition({ 'Cl': 0.35, 'F': 0.25 })) self.assertArrayAlmostEqual(s[1].frac_coords, [.5, .5, .5005]) # Test for TaS2 with spacegroup 166 in 160 setting. l = Lattice.from_lengths_and_angles([3.374351, 3.374351, 20.308941], [90.000000, 90.000000, 120.000000]) species = ["Ta", "S", "S"] coords = [[0.000000, 0.000000, 0.944333], [0.333333, 0.666667, 0.353424], [0.666667, 0.333333, 0.535243]] tas2 = Structure.from_spacegroup(160, l, species, coords) assert len(tas2) == 13 tas2.merge_sites(mode="d") assert len(tas2) == 9 l = Lattice.from_lengths_and_angles([3.587776, 3.587776, 19.622793], [90.000000, 90.000000, 120.000000]) species = ["Na", "V", "S", "S"] coords = [[0.333333, 0.666667, 0.165000], [0.000000, 0.000000, 0.998333], [0.333333, 0.666667, 0.399394], [0.666667, 0.333333, 0.597273]] navs2 = Structure.from_spacegroup(160, l, species, coords) assert len(navs2) == 18 navs2.merge_sites(mode="d") assert len(navs2) == 12 def test_properties(self): self.assertEqual(self.structure.num_sites, len(self.structure)) self.structure.make_supercell(2) self.structure[1] = "C" sites = list(self.structure.group_by_types()) self.assertEqual(sites[-1].specie.symbol, "C") self.structure.add_oxidation_state_by_element({"Si": 4, "C": 2}) self.assertEqual(self.structure.charge, 62) def test_set_item(self): s = self.structure.copy() s[0] = "C" self.assertEqual(s.formula, "Si1 C1") s[(0, 1)] = "Ge" self.assertEqual(s.formula, "Ge2") s[0:2] = "Sn" self.assertEqual(s.formula, "Sn2") s = self.structure.copy() s["Si"] = "C" self.assertEqual(s.formula, "C2") s["C"] = "C0.25Si0.5" self.assertEqual(s.formula, "Si1 C0.5") s["C"] = "C0.25Si0.5" self.assertEqual(s.formula, "Si1.25 C0.125") def test_init_error(self): self.assertRaises(StructureError, Structure, Lattice.cubic(3), ["Si"], [[0, 0, 0], [0.5, 0.5, 0.5]]) def test_from_sites(self): self.structure.add_site_property("hello", [1, 2]) s = Structure.from_sites(self.structure, to_unit_cell=True) self.assertEqual(s.site_properties["hello"][1], 2) def test_magic(self): s = Structure.from_sites(self.structure) self.assertEqual(s, self.structure) self.assertNotEqual(s, None) s.apply_strain(0.5) self.assertNotEqual(s, self.structure) self.assertNotEqual(self.structure * 2, self.structure)
def _generate_transformations( self, structure: Structure) -> Dict[str, MagOrderingTransformation]: """The central problem with trying to enumerate magnetic orderings is that we have to enumerate orderings that might plausibly be magnetic ground states, while not enumerating orderings that are physically implausible. The problem is that it is not always obvious by e.g. symmetry arguments alone which orderings to prefer. Here, we use a variety of strategies (heuristics) to enumerate plausible orderings, and later discard any duplicates that might be found by multiple strategies. This approach is not ideal, but has been found to be relatively robust over a wide range of magnetic structures. Args: structure: A sanitized input structure (_sanitize_input_structure) Returns: A dict of a transformation class instance (values) and name of enumeration strategy (keys) Returns: dict of Transformations keyed by strategy """ formula = structure.composition.reduced_formula transformations: Dict[str, MagOrderingTransformation] = {} # analyzer is used to obtain information on sanitized input analyzer = CollinearMagneticStructureAnalyzer( structure, default_magmoms=self.default_magmoms, overwrite_magmom_mode="replace_all", ) if not analyzer.is_magnetic: raise ValueError( "Not detected as magnetic, add a new default magmom for the element you believe may be magnetic?" ) # now we can begin to generate our magnetic orderings self.logger.info(f"Generating magnetic orderings for {formula}") mag_species_spin = analyzer.magnetic_species_and_magmoms types_mag_species = sorted( analyzer.types_of_magnetic_species, key=lambda sp: analyzer.default_magmoms.get(str(sp), 0), reverse=True, ) num_mag_sites = analyzer.number_of_magnetic_sites num_unique_sites = analyzer.number_of_unique_magnetic_sites() # enumerations become too slow as number of unique sites (and thus # permutations) increase, 8 is a soft limit, this can be increased # but do so with care if num_unique_sites > self.max_unique_sites: raise ValueError( "Too many magnetic sites to sensibly perform enumeration.") # maximum cell size to consider: as a rule of thumb, if the primitive cell # contains a large number of magnetic sites, perhaps we only need to enumerate # within one cell, whereas on the other extreme if the primitive cell only # contains a single magnetic site, we have to create larger supercells if "max_cell_size" not in self.transformation_kwargs: # TODO: change to 8 / num_mag_sites ? self.transformation_kwargs["max_cell_size"] = max( 1, int(4 / num_mag_sites)) self.logger.info("Max cell size set to {}".format( self.transformation_kwargs["max_cell_size"])) # when enumerating ferrimagnetic structures, it's useful to detect # symmetrically distinct magnetic sites, since different # local environments can result in different magnetic order # (e.g. inverse spinels) # initially, this was done by co-ordination number, but is # now done by a full symmetry analysis sga = SpacegroupAnalyzer(structure) structure_sym = sga.get_symmetrized_structure() wyckoff = ["n/a"] * len(structure) for indices, symbol in zip(structure_sym.equivalent_indices, structure_sym.wyckoff_symbols): for index in indices: wyckoff[index] = symbol is_magnetic_sites = [ site.specie in types_mag_species for site in structure ] # we're not interested in sites that we don't think are magnetic, # set these symbols to None to filter them out later wyckoff = [ symbol if is_magnetic_site else "n/a" for symbol, is_magnetic_site in zip(wyckoff, is_magnetic_sites) ] structure.add_site_property("wyckoff", wyckoff) wyckoff_symbols = set(wyckoff) - {"n/a"} # if user doesn't specifically request ferrimagnetic orderings, # we apply a heuristic as to whether to attempt them or not if self.automatic: if ("ferrimagnetic_by_motif" not in self.strategies and len(wyckoff_symbols) > 1 and len(types_mag_species) == 1): self.strategies += ["ferrimagnetic_by_motif"] if ("antiferromagnetic_by_motif" not in self.strategies and len(wyckoff_symbols) > 1 and len(types_mag_species) == 1): self.strategies += ["antiferromagnetic_by_motif"] if "ferrimagnetic_by_species" not in self.strategies and len( types_mag_species) > 1: self.strategies += ["ferrimagnetic_by_species"] # we start with a ferromagnetic ordering if "ferromagnetic" in self.strategies: # TODO: remove 0 spins ! fm_structure = analyzer.get_ferromagnetic_structure() # store magmom as spin property, to be consistent with output from # other transformations fm_structure.add_spin_by_site( fm_structure.site_properties["magmom"]) fm_structure.remove_site_property("magmom") # we now have our first magnetic ordering... self.ordered_structures.append(fm_structure) self.ordered_structure_origins.append("fm") # we store constraint(s) for each strategy first, # and then use each to perform a transformation later all_constraints: Dict[str, Any] = {} # ...to which we can add simple AFM cases first... if "antiferromagnetic" in self.strategies: constraint = MagOrderParameterConstraint( 0.5, # TODO: update MagOrderParameterConstraint in # pymatgen to take types_mag_species directly species_constraints=list(map(str, types_mag_species)), ) all_constraints["afm"] = [constraint] # allows for non-magnetic sublattices if len(types_mag_species) > 1: for sp in types_mag_species: constraints = [ MagOrderParameterConstraint( 0.5, species_constraints=str(sp)) ] all_constraints[f"afm_by_{sp}"] = constraints # ...and then we also try ferrimagnetic orderings by motif if a # single magnetic species is present... if "ferrimagnetic_by_motif" in self.strategies and len( wyckoff_symbols) > 1: # these orderings are AFM on one local environment, and FM on the rest for symbol in wyckoff_symbols: constraints = [ MagOrderParameterConstraint(0.5, site_constraint_name="wyckoff", site_constraints=symbol), MagOrderParameterConstraint( 1.0, site_constraint_name="wyckoff", site_constraints=list(wyckoff_symbols - {symbol}), ), ] all_constraints[f"ferri_by_motif_{symbol}"] = constraints # and also try ferrimagnetic when there are multiple magnetic species if "ferrimagnetic_by_species" in self.strategies: sp_list = [str(site.specie) for site in structure] num_sp = {sp: sp_list.count(str(sp)) for sp in types_mag_species} total_mag_sites = sum(num_sp.values()) for sp in types_mag_species: # attempt via a global order parameter all_constraints[ f"ferri_by_{sp}"] = num_sp[sp] / total_mag_sites # attempt via afm on sp, fm on remaining species constraints = [ MagOrderParameterConstraint(0.5, species_constraints=str(sp)), MagOrderParameterConstraint( 1.0, species_constraints=list( map(str, set(types_mag_species) - {sp})), ), ] all_constraints[f"ferri_by_{sp}_afm"] = constraints # ...and finally, we can try orderings that are AFM on one local # environment, and non-magnetic on the rest -- this is less common # but unless explicitly attempted, these states are unlikely to be found if "antiferromagnetic_by_motif" in self.strategies: for symbol in wyckoff_symbols: constraints = [ MagOrderParameterConstraint(0.5, site_constraint_name="wyckoff", site_constraints=symbol) ] all_constraints[f"afm_by_motif_{symbol}"] = constraints # and now construct all our transformations for each strategy transformations = {} for name, constraints in all_constraints.items(): trans = MagOrderingTransformation(mag_species_spin, order_parameter=constraints, **self.transformation_kwargs) transformations[name] = trans return transformations
def __init__( self, structure: Structure, overwrite_magmom_mode: Union[OverwriteMagmomMode, str] = "none", round_magmoms: bool = False, detect_valences: bool = False, make_primitive: bool = True, default_magmoms: bool = None, set_net_positive: bool = True, threshold: float = 0.1, ): """ A class which provides a few helpful methods to analyze collinear magnetic structures. If magnetic moments are not defined, moments will be taken either from default_magmoms.yaml (similar to the default magmoms in MPRelaxSet, with a few extra definitions) or from a specie:magmom dict provided by the default_magmoms kwarg. Input magmoms can be replaced using the 'overwrite_magmom_mode' kwarg. This can be: * "none" to do nothing, * "respect_sign" which will overwrite existing magmoms with those from default_magmoms but will keep sites with positive magmoms positive, negative magmoms negative and zero magmoms zero, * "respect_zeros", which will give a ferromagnetic structure (all positive magmoms from default_magmoms) but still keep sites with zero magmoms as zero, * "replace_all" which will try to guess initial magmoms for all sites in the structure irrespective of input structure (this is most suitable for an initial DFT calculation), * "replace_all_if_undefined" is the same as "replace_all" but only if no magmoms are defined in input structure, otherwise it will respect existing magmoms. * "normalize" will normalize magmoms to unity, but will respect sign (used for comparing orderings), magmoms < theshold will be set to zero :param structure: Structure object :param overwrite_magmom_mode (str): default "none" :param round_magmoms (int or bool): will round input magmoms to specified number of decimal places if integer is supplied, if set to a float will try and group magmoms together using a kernel density estimator of provided width, and extracting peaks of the estimator :param detect_valences (bool): if True, will attempt to assign valences to input structure :param make_primitive (bool): if True, will transform to primitive magnetic cell :param default_magmoms (dict): (optional) dict specifying default magmoms :param set_net_positive (bool): if True, will change sign of magnetic moments such that the net magnetization is positive. Argument will be ignored if mode "respect_sign" is used. :param threshold (float): number (in Bohr magnetons) below which magmoms will be rounded to zero, default of 0.1 can probably be increased for many magnetic systems, depending on your application """ if default_magmoms: self.default_magmoms = default_magmoms else: self.default_magmoms = DEFAULT_MAGMOMS structure = structure.copy() # check for disorder if not structure.is_ordered: raise NotImplementedError( "Not implemented for disordered structures, " "make ordered approximation first." ) if detect_valences: trans = AutoOxiStateDecorationTransformation() bva = BVAnalyzer() try: structure = trans.apply_transformation(structure) except ValueError: warnings.warn( "Could not assign valences " "for {}".format(structure.composition.reduced_formula) ) # check to see if structure has magnetic moments # on site properties or species spin properties, # prioritize site properties has_magmoms = bool(structure.site_properties.get("magmom", False)) has_spin = False for comp in structure.species_and_occu: for sp, occu in comp.items(): if getattr(sp, "spin", False): has_spin = True # perform input sanitation ... # rest of class will assume magnetic moments # are stored on site properties: # this is somewhat arbitrary, arguments can # be made for both approaches if has_magmoms and has_spin: raise ValueError( "Structure contains magnetic moments on both " "magmom site properties and spin species " "properties. This is ambiguous. Remove one or " "the other." ) elif has_magmoms: if None in structure.site_properties["magmom"]: warnings.warn( "Be careful with mixing types in your magmom " "site properties. Any 'None' magmoms have been " "replaced with zero." ) magmoms = [m if m else 0 for m in structure.site_properties["magmom"]] elif has_spin: magmoms = [getattr(sp, "spin", 0) for sp in structure.species] structure.remove_spin() else: # no magmoms present, add zero magmoms for now magmoms = [0] * len(structure) # and overwrite magmoms with default magmoms later unless otherwise stated if overwrite_magmom_mode == "replace_all_if_undefined": overwrite_magmom_mode = "replace_all" # test to see if input structure has collinear magmoms self.is_collinear = Magmom.are_collinear(magmoms) if not self.is_collinear: warnings.warn( "This class is not designed to be used with " "non-collinear structures. If your structure is " "only slightly non-collinear (e.g. canted) may still " "give useful results, but use with caution." ) # this is for collinear structures only, make sure magmoms # are all floats magmoms = list(map(float, magmoms)) # set properties that should be done /before/ we process input magmoms self.total_magmoms = sum(magmoms) self.magnetization = sum(magmoms) / structure.volume # round magmoms below threshold to zero magmoms = [m if abs(m) > threshold else 0 for m in magmoms] # overwrite existing magmoms with default_magmoms if overwrite_magmom_mode not in ( "none", "respect_sign", "respect_zeros", "replace_all", "replace_all_if_undefined", "normalize", ): raise ValueError("Unsupported mode.") for idx, site in enumerate(structure): if site.species_string in self.default_magmoms: # look for species first, e.g. Fe2+ default_magmom = self.default_magmoms[site.species_string] elif ( isinstance(site.specie, Specie) and str(site.specie.element) in self.default_magmoms ): # look for element, e.g. Fe default_magmom = self.default_magmoms[str(site.specie.element)] else: default_magmom = 0 # overwrite_magmom_mode = "respect_sign" will change magnitude of # existing moments only, and keep zero magmoms as # zero: it will keep the magnetic ordering intact if overwrite_magmom_mode == "respect_sign": set_net_positive = False if magmoms[idx] > 0: magmoms[idx] = default_magmom elif magmoms[idx] < 0: magmoms[idx] = -default_magmom # overwrite_magmom_mode = "respect_zeros" will give a ferromagnetic # structure but will keep zero magmoms as zero elif overwrite_magmom_mode == "respect_zeros": if magmoms[idx] != 0: magmoms[idx] = default_magmom # overwrite_magmom_mode = "replace_all" will ignore input magmoms # and give a ferromagnetic structure with magnetic # moments on *all* atoms it thinks could be magnetic elif overwrite_magmom_mode == "replace_all": magmoms[idx] = default_magmom # overwrite_magmom_mode = "normalize" set magmoms magnitude to 1 elif overwrite_magmom_mode == "normalize": if magmoms[idx] != 0: magmoms[idx] = int(magmoms[idx] / abs(magmoms[idx])) # round magmoms, used to smooth out computational data magmoms = ( self._round_magmoms(magmoms, round_magmoms) if round_magmoms else magmoms ) if set_net_positive: sign = np.sum(magmoms) if sign < 0: magmoms = -np.array(magmoms) structure.add_site_property("magmom", magmoms) if make_primitive: structure = structure.get_primitive_structure(use_site_props=True) self.structure = structure
def _get_structure(self, data, primitive): """ Generate structure from part of the cif. """ def get_num_implicit_hydrogens(sym): num_h = {"Wat": 2, "wat": 2, "O-H": 1} return num_h.get(sym[:3], 0) lattice = self.get_lattice(data) # if magCIF, get magnetic symmetry moments and magmoms # else standard CIF, and use empty magmom dict if self.feature_flags["magcif_incommensurate"]: raise NotImplementedError( "Incommensurate structures not currently supported.") elif self.feature_flags["magcif"]: self.symmetry_operations = self.get_magsymops(data) magmoms = self.parse_magmoms(data, lattice=lattice) else: self.symmetry_operations = self.get_symops(data) magmoms = {} oxi_states = self.parse_oxi_states(data) coord_to_species = OrderedDict() coord_to_magmoms = OrderedDict() def get_matching_coord(coord): keys = list(coord_to_species.keys()) coords = np.array(keys) for op in self.symmetry_operations: c = op.operate(coord) inds = find_in_coord_list_pbc(coords, c, atol=self._site_tolerance) # cant use if inds, because python is dumb and np.array([0]) evaluates # to False if len(inds): return keys[inds[0]] return False label_el_dict = {} for i in range(len(data["_atom_site_label"])): try: # If site type symbol exists, use it. Otherwise, we use the # label. symbol = self._parse_symbol(data["_atom_site_type_symbol"][i]) label = data["_atom_site_label"][i] num_h = get_num_implicit_hydrogens( data["_atom_site_type_symbol"][i]) except KeyError: symbol = self._parse_symbol(data["_atom_site_label"][i]) label = data["_atom_site_label"][i] num_h = get_num_implicit_hydrogens(data["_atom_site_label"][i]) if not symbol: continue if oxi_states is not None: o_s = oxi_states.get(symbol, 0) # use _atom_site_type_symbol if possible for oxidation state if "_atom_site_type_symbol" in data.data.keys(): oxi_symbol = data["_atom_site_type_symbol"][i] o_s = oxi_states.get(oxi_symbol, o_s) try: el = Specie(symbol, o_s) except: el = DummySpecie(symbol, o_s) else: el = get_el_sp(symbol) x = str2float(data["_atom_site_fract_x"][i]) y = str2float(data["_atom_site_fract_y"][i]) z = str2float(data["_atom_site_fract_z"][i]) magmom = magmoms.get(data["_atom_site_label"][i], np.array([0, 0, 0])) try: occu = str2float(data["_atom_site_occupancy"][i]) except (KeyError, ValueError): occu = 1 if occu > 0: coord = (x, y, z) match = get_matching_coord(coord) comp_d = {el: occu} if num_h > 0: comp_d["H"] = num_h comp = Composition(comp_d) if not match: coord_to_species[coord] = comp coord_to_magmoms[coord] = magmom else: coord_to_species[match] += comp # disordered magnetic not currently supported coord_to_magmoms[match] = None label_el_dict[coord] = label sum_occu = [ sum(c.values()) for c in coord_to_species.values() if not set(c.elements) == {Element("O"), Element("H")} ] if any([o > 1 for o in sum_occu]): msg = "Some occupancies (%s) sum to > 1! If they are within " \ "the tolerance, they will be rescaled." % str(sum_occu) warnings.warn(msg) self.errors.append(msg) allspecies = [] allcoords = [] allmagmoms = [] allhydrogens = [] alllabels = [] # check to see if magCIF file is disordered if self.feature_flags["magcif"]: for k, v in coord_to_magmoms.items(): if v is None: # Proposed solution to this is to instead store magnetic # moments as Specie 'spin' property, instead of site # property, but this introduces ambiguities for end user # (such as unintended use of `spin` and Specie will have # fictious oxidation state). raise NotImplementedError( 'Disordered magnetic structures not currently supported.' ) if coord_to_species.items(): for comp, group in groupby(sorted(list(coord_to_species.items()), key=lambda x: x[1]), key=lambda x: x[1]): tmp_coords = [site[0] for site in group] #print(tmp_coords) labels = [] for i in tmp_coords: labels.append(label_el_dict[i]) #print(labels) tmp_magmom = [ coord_to_magmoms[tmp_coord] for tmp_coord in tmp_coords ] if self.feature_flags["magcif"]: coords, magmoms, coords_num = self._unique_coords( tmp_coords, magmoms_in=tmp_magmom, lattice=lattice) else: coords, magmoms, coords_num = self._unique_coords( tmp_coords) if set(comp.elements) == {Element("O"), Element("H")}: # O with implicit hydrogens im_h = comp["H"] species = Composition({"O": comp["O"]}) else: im_h = 0 species = comp allhydrogens.extend(len(coords) * [im_h]) allcoords.extend(coords) allspecies.extend(len(coords) * [species]) allmagmoms.extend(magmoms) for i in range(len(coords_num)): alllabels.extend(coords_num[i] * [labels[i]]) # rescale occupancies if necessary for i, species in enumerate(allspecies): totaloccu = sum(species.values()) if 1 < totaloccu <= self._occupancy_tolerance: allspecies[i] = species / totaloccu if allspecies and len(allspecies) == len(allcoords) \ and len(allspecies) == len(allmagmoms): site_properties = dict() if any(allhydrogens): assert len(allhydrogens) == len(allcoords) site_properties["implicit_hydrogens"] = allhydrogens if self.feature_flags["magcif"]: site_properties["magmom"] = allmagmoms if len(site_properties) == 0: site_properties = None struct = Structure(lattice, allspecies, allcoords, site_properties=site_properties) #struct = struct.get_sorted_structure() if primitive and self.feature_flags['magcif']: struct = struct.get_primitive_structure(use_site_props=True) elif primitive: struct = struct.get_primitive_structure() struct = struct.get_reduced_structure() struct.add_site_property("_atom_site_label", alllabels) return struct