diff --git a/aiida/backends/tests/dataclasses.py b/aiida/backends/tests/dataclasses.py index f9cf231e38..aa7df750d8 100644 --- a/aiida/backends/tests/dataclasses.py +++ b/aiida/backends/tests/dataclasses.py @@ -1932,6 +1932,9 @@ class TestStructureDataFromAse(AiidaTestCase): @unittest.skipIf(not has_ase(), "Unable to import ase") def test_ase(self): + """ + Tests roundtrip ASE -> StructureData -> ASE + """ from aiida.orm.data.structure import StructureData import ase @@ -1957,6 +1960,9 @@ def test_ase(self): @unittest.skipIf(not has_ase(), "Unable to import ase") def test_conversion_of_types_1(self): + """ + Tests roundtrip ASE -> StructureData -> ASE, with tags + """ from aiida.orm.data.structure import StructureData import ase @@ -1987,6 +1993,10 @@ def test_conversion_of_types_1(self): @unittest.skipIf(not has_ase(), "Unable to import ase") def test_conversion_of_types_2(self): + """ + Tests roundtrip ASE -> StructureData -> ASE, with tags, and + changing the atomic masses + """ from aiida.orm.data.structure import StructureData import ase @@ -2019,6 +2029,9 @@ def test_conversion_of_types_2(self): @unittest.skipIf(not has_ase(), "Unable to import ase") def test_conversion_of_types_3(self): + """ + Tests StructureData -> ASE, with all sorts of kind names + """ from aiida.orm.data.structure import StructureData a = StructureData() @@ -2050,6 +2063,9 @@ def test_conversion_of_types_3(self): @unittest.skipIf(not has_ase(), "Unable to import ase") def test_conversion_of_types_4(self): + """ + Tests ASE -> StructureData -> ASE, in particular conversion tags / kind names + """ from aiida.orm.data.structure import StructureData import ase @@ -2060,9 +2076,18 @@ def test_conversion_of_types_4(self): s = StructureData(ase=atoms) kindnames = set([k.name for k in s.kinds]) self.assertEquals(kindnames, set(['Fe', 'Fe1', 'Fe4'])) + # check roundtrip ASE -> StructureData -> ASE + atoms2 = s.get_ase() + self.assertEquals(list(atoms2.get_tags()),list(atoms.get_tags())) + self.assertEquals(list(atoms2.get_chemical_symbols()),list(atoms.get_chemical_symbols())) + self.assertEquals(atoms2.get_chemical_formula(),'Fe5') @unittest.skipIf(not has_ase(), "Unable to import ase") def test_conversion_of_types_5(self): + """ + Tests ASE -> StructureData -> ASE, in particular conversion tags / kind names + (subtle variation of test_conversion_of_types_4) + """ from aiida.orm.data.structure import StructureData import ase @@ -2073,6 +2098,34 @@ def test_conversion_of_types_5(self): s = StructureData(ase=atoms) kindnames = set([k.name for k in s.kinds]) self.assertEquals(kindnames, set(['Fe', 'Fe1', 'Fe4'])) + # check roundtrip ASE -> StructureData -> ASE + atoms2 = s.get_ase() + self.assertEquals(list(atoms2.get_tags()),list(atoms.get_tags())) + self.assertEquals(list(atoms2.get_chemical_symbols()),list(atoms.get_chemical_symbols())) + self.assertEquals(atoms2.get_chemical_formula(),'Fe5') + + @unittest.skipIf(not has_ase(), "Unable to import ase") + def test_conversion_of_types_6(self): + """ + Tests roundtrip StructureData -> ASE -> StructureData, with tags/kind names + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[4,0,0],[0,4,0],[0,0,4]]) + a.append_atom(position=(0,0,0), symbols='Ni', name='Ni1') + a.append_atom(position=(2,2,2), symbols='Ni', name='Ni2') + a.append_atom(position=(1,0,1), symbols='Cl', name='Cl') + a.append_atom(position=(1,3,1), symbols='Cl', name='Cl') + + b = a.get_ase() + self.assertEquals(b.get_chemical_symbols(), ['Ni', 'Ni', 'Cl','Cl']) + self.assertEquals(list(b.get_tags()), [1, 2, 0, 0]) + + c = StructureData(ase=b) + self.assertEquals(c.get_site_kindnames(), ['Ni1', 'Ni2', 'Cl','Cl']) + self.assertEquals([k.symbol for k in c.kinds], ['Ni', 'Ni', 'Cl']) + self.assertEquals([s.position for s in c.sites], + [(0.,0.,0.),(2.,2.,2.),(1.,0.,1.),(1.,3.,1.)]) class TestStructureDataFromPymatgen(AiidaTestCase): @@ -2090,7 +2143,8 @@ class TestStructureDataFromPymatgen(AiidaTestCase): "Mismatch in the version of pymatgen (expected 4.5.3)") def test_1(self): """ - Test's imput is derived from COD entry 9011963, processed with + Tests roundtrip pymatgen -> StructureData -> pymatgen + Test's input is derived from COD entry 9011963, processed with cif_mark_disorder (from cod-tools) and abbreviated. """ from aiida.orm.data.structure import StructureData @@ -2162,6 +2216,7 @@ def test_1(self): "Mismatch in the version of pymatgen (expected 4.5.3)") def test_2(self): """ + Tests xyz -> pymatgen -> StructureData Input source: http://pymatgen.org/_static/Molecule.html """ from aiida.orm.data.structure import StructureData @@ -2202,6 +2257,42 @@ def test_2(self): [round(x, 2) for x in list(struct.sites[4].position)], [5.77, 5.89, 5.73]) + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_partial_occ_and_spin(self): + """ + Tests pymatgen -> StructureData, with partial occupancies and spins. + This should raise a ValueError. + """ + from aiida.orm.data.structure import StructureData + import pymatgen + + Fe_spin_up = pymatgen.structure.Specie('Fe',0,properties={'spin':1}) + Mn_spin_up = pymatgen.structure.Specie('Mn',0,properties={'spin':1}) + Fe_spin_down = pymatgen.structure.Specie('Fe',0,properties={'spin':-1}) + Mn_spin_down = pymatgen.structure.Specie('Mn',0,properties={'spin':-1}) + FeMn1 = pymatgen.Composition({Fe_spin_up:0.5,Mn_spin_up:0.5}) + FeMn2 = pymatgen.Composition({Fe_spin_down:0.5,Mn_spin_down:0.5}) + a = pymatgen.structure.Structure(lattice=[[4,0,0],[0,4,0],[0,0,4]], + species=[FeMn1,FeMn2], + coords=[[0,0,0],[0.5,0.5,0.5]]) + + with self.assertRaises(ValueError): + StructureData(pymatgen=a) + + # same, with vacancies + Fe1 = pymatgen.Composition({Fe_spin_up:0.5}) + Fe2 = pymatgen.Composition({Fe_spin_down:0.5}) + a = pymatgen.structure.Structure(lattice=[[4,0,0],[0,4,0],[0,0,4]], + species=[Fe1,Fe2], + coords=[[0,0,0],[0.5,0.5,0.5]]) + + with self.assertRaises(ValueError): + StructureData(pymatgen=a) + class TestPymatgenFromStructureData(AiidaTestCase): """ @@ -2219,7 +2310,7 @@ class TestPymatgenFromStructureData(AiidaTestCase): "Mismatch in the version of pymatgen (expected 4.5.3)") def test_1(self): """ - Test the check of periodic boundary conditions. + Tests the check of periodic boundary conditions. """ from aiida.orm.data.structure import StructureData @@ -2239,6 +2330,9 @@ def test_1(self): StrictVersion('4.5.3'), "Mismatch in the version of pymatgen (expected 4.5.3)") def test_2(self): + """ + Tests ASE -> StructureData -> pymatgen + """ from aiida.orm.data.structure import StructureData import ase @@ -2274,7 +2368,8 @@ def test_2(self): "Mismatch in the version of pymatgen (expected 4.5.3)") def test_3(self): """ - Test the conversion of StructureData to pymatgen's Molecule. + Tests the conversion of StructureData to pymatgen's Molecule + (ASE -> StructureData -> pymatgen) """ from aiida.orm.data.structure import StructureData import ase @@ -2299,7 +2394,203 @@ def test_3(self): [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]]) + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_roundtrip(self): + """ + Tests roundtrip StructureData -> pymatgen -> StructureData + (no spins) + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[5.6,0,0],[0,5.6,0],[0,0,5.6]]) + a.append_atom(position=(0,0,0), symbols='Cl') + a.append_atom(position=(2.8,0,2.8), symbols='Cl') + a.append_atom(position=(0,2.8,2.8), symbols='Cl') + a.append_atom(position=(2.8,2.8,0), symbols='Cl') + a.append_atom(position=(2.8,2.8,2.8), symbols='Na') + a.append_atom(position=(2.8,0,0), symbols='Na') + a.append_atom(position=(0,2.8,0), symbols='Na') + a.append_atom(position=(0,0,2.8), symbols='Na') + + b = a.get_pymatgen() + c = StructureData(pymatgen=b) + self.assertEquals(c.get_site_kindnames(), ['Cl','Cl','Cl','Cl','Na','Na','Na','Na']) + self.assertEquals([k.symbol for k in c.kinds], ['Cl','Na']) + self.assertEquals([s.position for s in c.sites], + [(0.,0.,0.),(2.8,0,2.8),(0,2.8,2.8),(2.8,2.8,0),(2.8,2.8,2.8),(2.8,0,0),(0,2.8,0),(0,0,2.8)]) + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_roundtrip_kindnames(self): + """ + Tests roundtrip StructureData -> pymatgen -> StructureData + (no spins, but with all kind of kind names) + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[5.6,0,0],[0,5.6,0],[0,0,5.6]]) + a.append_atom(position=(0,0,0), symbols='Cl',name='Cl') + a.append_atom(position=(2.8,0,2.8), symbols='Cl',name='Cl10') + a.append_atom(position=(0,2.8,2.8), symbols='Cl',name='Cla') + a.append_atom(position=(2.8,2.8,0), symbols='Cl',name='cl_x') + a.append_atom(position=(2.8,2.8,2.8), symbols='Na',name='Na1') + a.append_atom(position=(2.8,0,0), symbols='Na',name='Na2') + a.append_atom(position=(0,2.8,0), symbols='Na',name='Na_Na') + a.append_atom(position=(0,0,2.8), symbols='Na',name='Na4') + + b = a.get_pymatgen() + self.assertEquals([site.properties['kind_name'] for site in b.sites], + ['Cl','Cl10','Cla','cl_x','Na1','Na2','Na_Na','Na4']) + + c = StructureData(pymatgen=b) + self.assertEquals(c.get_site_kindnames(), ['Cl','Cl10','Cla','cl_x','Na1','Na2','Na_Na','Na4']) + self.assertEquals(c.get_symbols_set(), set(['Cl','Na'])) + self.assertEquals([s.position for s in c.sites], + [(0.,0.,0.),(2.8,0,2.8),(0,2.8,2.8),(2.8,2.8,0),(2.8,2.8,2.8),(2.8,0,0),(0,2.8,0),(0,0,2.8)]) + + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_roundtrip_spins(self): + """ + Tests roundtrip StructureData -> pymatgen -> StructureData + (with spins) + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[5.6,0,0],[0,5.6,0],[0,0,5.6]]) + a.append_atom(position=(0,0,0), symbols='Mn',name='Mn1') + a.append_atom(position=(2.8,0,2.8), symbols='Mn',name='Mn1') + a.append_atom(position=(0,2.8,2.8), symbols='Mn',name='Mn1') + a.append_atom(position=(2.8,2.8,0), symbols='Mn',name='Mn1') + a.append_atom(position=(2.8,2.8,2.8), symbols='Mn',name='Mn2') + a.append_atom(position=(2.8,0,0), symbols='Mn',name='Mn2') + a.append_atom(position=(0,2.8,0), symbols='Mn',name='Mn2') + a.append_atom(position=(0,0,2.8), symbols='Mn',name='Mn2') + + b = a.get_pymatgen(add_spin=True) + # check the spins + self.assertEquals([s.as_dict()['properties']['spin'] for s in b.species], + [-1, -1, -1, -1, 1, 1, 1, 1]) + # back to StructureData + c = StructureData(pymatgen=b) + self.assertEquals(c.get_site_kindnames(), ['Mn1','Mn1','Mn1','Mn1','Mn2','Mn2','Mn2','Mn2']) + self.assertEquals([k.symbol for k in c.kinds], ['Mn','Mn']) + self.assertEquals([s.position for s in c.sites], + [(0.,0.,0.),(2.8,0,2.8),(0,2.8,2.8),(2.8,2.8,0),(2.8,2.8,2.8),(2.8,0,0),(0,2.8,0),(0,0,2.8)]) + + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_roundtrip_partial_occ(self): + """ + Tests roundtrip StructureData -> pymatgen -> StructureData + (with partial occupancies). + Structure initially from ICSD (id: 251993). + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[3.9912, 0.0, 0.0], + [-1.9956, 3.456480591584452, 0.0], + [0.0, 0.0, 16.2958]]) + a.append_atom(position=(0.0,0.0,13.49455198), symbols='Fe') + a.append_atom(position=(0.0,0.0,2.80124802), symbols='Fe') + a.append_atom(position=(0.0,0.0,5.34665198), symbols='Fe') + a.append_atom(position=(0.0,0.0,10.94914802), symbols='Fe') + a.append_atom(position=(1.9956,1.15239062923,12.22185), symbols='Fe',weights=0.9) + a.append_atom(position=(0.0,2.30408996235,4.07395), symbols='Fe',weights=0.9) + a.append_atom(position=(0.0,2.30408996235,12.22185),symbols='Ge') + a.append_atom(position=(1.9956,1.15239062923,4.07395),symbols='Ge') + a.append_atom(position=(1.9956,1.15239062923,14.8373259),symbols='Te') + a.append_atom(position=(0.0,2.30408996235,1.4584741),symbols='Te') + a.append_atom(position=(0.0,2.30408996235,6.6894259),symbols='Te') + a.append_atom(position=(1.9956,1.15239062923,9.6063741),symbols='Te') + + # a few checks on the structure kinds and symbols + self.assertEquals(a.get_symbols_set(),set(['Fe', 'Ge', 'Te'])) + self.assertEquals(a.get_site_kindnames(), + ['Fe','Fe','Fe','Fe','FeX','FeX','Ge','Ge','Te','Te','Te','Te']) + self.assertEquals(a.get_formula(),'Fe4Ge2Te4{Fe0.90X0.10}2') + + b = a.get_pymatgen() + # check the partial occupancies + self.assertEquals([s.as_dict() for s in b.species_and_occu], + [{'Fe':1.0},{'Fe':1.0},{'Fe':1.0},{'Fe':1.0}, + {'Fe':0.9},{'Fe':0.9},{'Ge':1.0},{'Ge':1.0}, + {'Te':1.0},{'Te':1.0},{'Te':1.0},{'Te':1.0}]) + + # back to StructureData + c = StructureData(pymatgen=b) + self.assertEquals(c.cell,[[3.9912, 0.0, 0.0], + [-1.9956, 3.456480591584452, 0.0], + [0.0, 0.0, 16.2958]]) + self.assertEquals(c.get_symbols_set(),set(['Fe', 'Ge', 'Te'])) + self.assertEquals(c.get_site_kindnames(), + ['Fe','Fe','Fe','Fe','FeX','FeX','Ge','Ge','Te','Te','Te','Te']) + self.assertEquals(c.get_formula(),'Fe4Ge2Te4{Fe0.90X0.10}2') + self.assertEquals([s.position for s in c.sites], + [(0.0, 0.0, 13.49455198), + (0.0, 0.0, 2.80124802), + (0.0, 0.0, 5.34665198), + (0.0, 0.0, 10.94914802), + (1.9956, 1.15239062923, 12.22185), + (0.0, 2.30408996235, 4.07395), + (0.0, 2.30408996235, 12.22185), + (1.9956, 1.15239062923, 4.07395), + (1.9956, 1.15239062923, 14.8373259), + (0.0, 2.30408996235, 1.4584741), + (0.0, 2.30408996235, 6.6894259), + (1.9956, 1.15239062923, 9.6063741)]) + + @unittest.skipIf(not has_pymatgen(), "Unable to import pymatgen") + @unittest.skipIf(has_pymatgen() and + StrictVersion(get_pymatgen_version()) != + StrictVersion('4.5.3'), + "Mismatch in the version of pymatgen (expected 4.5.3)") + def test_partial_occ_and_spin(self): + """ + Tests StructureData -> pymatgen, with partial occupancies and spins. + This should raise a ValueError. + """ + from aiida.orm.data.structure import StructureData + + a = StructureData(cell=[[4,0,0],[0,4,0],[0,0,4]]) + a.append_atom(position=(0,0,0), symbols=('Fe','Al'),weights=(0.8,0.2),name='FeAl1') + a.append_atom(position=(2,2,2), symbols=('Fe','Al'),weights=(0.8,0.2),name='FeAl2') + + # a few checks on the structure kinds and symbols + self.assertEquals(a.get_symbols_set(),set(['Fe', 'Al'])) + self.assertEquals(a.get_site_kindnames(),['FeAl1','FeAl2']) + self.assertEquals(a.get_formula(),'{Al0.20Fe0.80}2') + + with self.assertRaises(ValueError): + a.get_pymatgen(add_spin=True) + + # same, with vacancies + a = StructureData(cell=[[4,0,0],[0,4,0],[0,0,4]]) + a.append_atom(position=(0,0,0), symbols='Fe',weights=0.8,name='FeX1') + a.append_atom(position=(2,2,2), symbols='Fe',weights=0.8,name='FeX2') + + # a few checks on the structure kinds and symbols + self.assertEquals(a.get_symbols_set(),set(['Fe'])) + self.assertEquals(a.get_site_kindnames(),['FeX1','FeX2']) + self.assertEquals(a.get_formula(),'{Fe0.80X0.20}2') + + with self.assertRaises(ValueError): + a.get_pymatgen(add_spin=True) + + class TestArrayData(AiidaTestCase): """ Tests the ArrayData objects. diff --git a/aiida/orm/data/structure.py b/aiida/orm/data/structure.py index b762196f7b..1aa56ec073 100644 --- a/aiida/orm/data/structure.py +++ b/aiida/orm/data/structure.py @@ -182,7 +182,21 @@ def _create_weights_tuple(weights): weights_tuple = tuple(float(i) for i in weights) return weights_tuple - + +def create_automatic_kind_name(symbols,weights): + """ + Create a string obtained with the symbols appended one + after the other, without spaces, in alphabetical order; + if the site has a vacancy, a X is appended at the end too. + """ + sorted_symbol_list = list(set(symbols)) + sorted_symbol_list.sort() # In-place sort + name_string = "".join(sorted_symbol_list) + if has_vacancies(weights): + name_string += "X" + return name_string + + def validate_weights_tuple(weights_tuple, threshold): """ Validates the weight of the atomic kinds. @@ -529,24 +543,9 @@ def get_formula(symbol_list, mode='hill', separator=""): 'reduce, count or count_compact') if mode in ['hill_compact', 'count_compact']: - - def gcd_list(int_list): - """ - Recursive function to get the greatest common divisor of - a list of integers - """ - from fractions import gcd - if len(int_list) == 1: - return int_list[0] - elif len(int_list) == 2: - return gcd(int_list[0], int_list[1]) - else: - the_int_list = int_list[2:] - the_int_list.append(gcd(int_list[0], int_list[1])) - return gcd_list(the_int_list) - - the_gcd = gcd_list([e[0] for e in the_symbol_list]) - the_symbol_list = [[e[0] / the_gcd, e[1]] for e in the_symbol_list] + from fractions import gcd + the_gcd = reduce(gcd,[e[0] for e in the_symbol_list]) + the_symbol_list = [[e[0]/the_gcd,e[1]] for e in the_symbol_list] return get_formula_from_symbol_list(the_symbol_list, separator=separator) @@ -830,17 +829,51 @@ def set_pymatgen_structure(self, struct): .. note:: periodic boundary conditions are set to True in all three directions. - .. note:: Requires the pymatgen module (version >= 3.0.13, usage + .. note:: Requires the pymatgen module (version >= 3.3.5, usage of earlier versions may cause errors). + + :raise ValueError: if there are partial occupancies together with spins. """ + def build_kind_name(species_and_occu): + """ + Build a kind name from a pymatgen Composition, including an + additional ordinal if spin is included, e.g. it returns '1' + for an atom with spin<0 and '2' for an element with spin>0, + ortherwise (no spin) it returns simply '' + :param specie: a pymatgen specie + :return: a string + """ + has_spin = any([specie.as_dict().get('properties',{}).get('spin',0)!=0 + for specie in species_and_occu.keys()]) + + if has_spin and (len(species_and_occu.items())>1 + or any([weight!=1.0 for weight in species_and_occu.values()])): + raise ValueError("Cannot set partial occupancies and spins " + "at the same time") + + if not has_spin: + return Kind(symbols=[x[0].symbol for x in species_and_occu.items()], + weights=[x[1] for x in species_and_occu.items()]).name + + else: + spin = species_and_occu.keys()[0].as_dict().get('properties',{}).get('spin',0) + if spin<0: + return specie.symbol+'1' + else: + return specie.symbol+'2' + self.cell = struct.lattice.matrix.tolist() self.pbc = [True, True, True] self.clear_kinds() for site in struct.sites: - self.append_atom( - symbols=[x[0].symbol for x in site.species_and_occu.items()], - weights=[x[1] for x in site.species_and_occu.items()], - position=site.coords.tolist()) + if 'kind_name' in site.properties: + kind_name = site.properties['kind_name'] + else: + kind_name = build_kind_name(site.species_and_occu) + self.append_atom(symbols=[x[0].symbol for x in site.species_and_occu.items()], + weights=[x[1] for x in site.species_and_occu.items()], + position=site.coords.tolist(), + name=kind_name) def _validate(self): """ @@ -1143,20 +1176,36 @@ def get_ase(self): """ return self._get_object_ase() - def get_pymatgen(self): + def get_pymatgen(self,**kwargs): """ Get pymatgen object. Returns Structure for structures with periodic boundary conditions (in three dimensions) and Molecule otherwise. + :param add_spin: True to add the spins to the pymatgen structure. + Default is False (no spin added). + + .. note:: The spins are set according to the following rule: + + * if the kind name ends with 1 -> spin=+1 + + * if the kind name ends with 2 -> spin=-1 .. note:: Requires the pymatgen module (version >= 3.0.13, usage of earlier versions may cause errors). """ - return self._get_object_pymatgen() + return self._get_object_pymatgen(**kwargs) - def get_pymatgen_structure(self): + def get_pymatgen_structure(self,**kwargs): """ Get the pymatgen Structure object. + :param add_spin: True to add the spins to the pymatgen structure. + Default is False (no spin added). + + .. note:: The spins are set according to the following rule: + + * if the kind name ends with 1 -> spin=+1 + + * if the kind name ends with 2 -> spin=-1 .. note:: Requires the pymatgen module (version >= 3.0.13, usage of earlier versions may cause errors). @@ -1167,7 +1216,7 @@ def get_pymatgen_structure(self): :raise ValueError: if periodic boundary conditions do not hold in at least one dimension of real space. """ - return self._get_object_pymatgen_structure() + return self._get_object_pymatgen_structure(**kwargs) def get_pymatgen_molecule(self): """ @@ -1743,7 +1792,7 @@ def _get_object_ase(self): asecell.append(site.get_ase(kinds=_kinds)) return asecell - def _get_object_pymatgen(self): + def _get_object_pymatgen(self,**kwargs): """ Converts :py:class:`StructureData ` @@ -1756,21 +1805,30 @@ def _get_object_pymatgen(self): of earlier versions may cause errors). """ if self.pbc == (True, True, True): - return self._get_object_pymatgen_structure() + return self._get_object_pymatgen_structure(**kwargs) else: - return self._get_object_pymatgen_molecule() + return self._get_object_pymatgen_molecule(**kwargs) - def _get_object_pymatgen_structure(self): + def _get_object_pymatgen_structure(self,**kwargs): """ Converts :py:class:`StructureData ` to pymatgen Structure object + :param add_spin: True to add the spins to the pymatgen structure. + Default is False (no spin added). + + .. note:: The spins are set according to the following rule: + + * if the kind name ends with 1 -> spin=+1 + + * if the kind name ends with 2 -> spin=-1 :return: a pymatgen Structure object corresponding to this :py:class:`StructureData ` object :raise ValueError: if periodic boundary conditions does not hold - in at least one dimension of real space + in at least one dimension of real space; if there are partial occupancies + together with spins (defined by kind names ending with '1' or '2'). .. note:: Requires the pymatgen module (version >= 3.0.13, usage of earlier versions may cause errors) @@ -1782,15 +1840,41 @@ def _get_object_pymatgen_structure(self): "all three dimensions of real space") species = [] - for s in self.sites: - k = self.get_kind(s.kind_name) - species.append({s: w for s, w in zip(k.symbols, k.weights)}) + additional_kwargs = {} + + if (kwargs.pop('add_spin',False) and + any([n.endswith('1') or n.endswith('2') for n in self.get_kind_names()])): + # case when spins are defined -> no partial occupancy allowed + from pymatgen.core.structure import Specie + oxidation_state = 0 # now I always set the oxidation_state to zero + for s in self.sites: + k = self.get_kind(s.kind_name) + if len(k.symbols)!=1 or (len(k.weights)!=1 or sum(k.weights)<1.): + raise ValueError("Cannot set partial occupancies and spins " + "at the same time") + species.append(Specie(k.symbols[0],oxidation_state, + properties={'spin': -1 if k.name.endswith('1') + else 1 if k.name.endswith('2') else 0})) + else: + # case when no spin are defined + for s in self.sites: + k = self.get_kind(s.kind_name) + species.append({s: w for s, w in zip(k.symbols, k.weights)}) + if any([create_automatic_kind_name(self.get_kind(name).symbols,self.get_kind(name).weights)!=name + for name in self.get_site_kindnames()]): + # add "kind_name" as a properties to each site, whenever + # the kind_name cannot be automatically obtained from the symbols + additional_kwargs['site_properties'] = {'kind_name': self.get_site_kindnames()} + + if kwargs: + raise ValueError("Unrecognized parameters passed to pymatgen " + "converter: {}".format(kwargs.keys())) positions = [list(x.position) for x in self.sites] return Structure(self.cell, species, positions, - coords_are_cartesian=True) + coords_are_cartesian=True,**additional_kwargs) - def _get_object_pymatgen_molecule(self): + def _get_object_pymatgen_molecule(self,**kwargs): """ Converts :py:class:`StructureData ` @@ -1805,6 +1889,10 @@ def _get_object_pymatgen_molecule(self): """ from pymatgen.core.structure import Molecule + if kwargs: + raise ValueError("Unrecognized parameters passed to pymatgen " + "converter: {}".format(kwargs.keys())) + species = [] for s in self.sites: k = self.get_kind(s.kind_name) @@ -2022,11 +2110,7 @@ def set_automatic_kind_name(self, tag=None): after the other, without spaces, in alphabetical order; if the site has a vacancy, a X is appended at the end too. """ - sorted_symbol_list = list(set(self.symbols)) - sorted_symbol_list.sort() # In-place sort - name_string = "".join(sorted_symbol_list) - if self.has_vacancies(): - name_string += "X" + name_string = create_automatic_kind_name(self.symbols,self.weights) if tag is None: self.name = name_string else: