1# coding: utf-8
2# Copyright (c) Pymatgen Development Team.
3# Distributed under the terms of the MIT License.
4
5
6import unittest
7import warnings
8
9import numpy as np
10
11from pymatgen.core.composition import Composition
12from pymatgen.core.periodic_table import DummySpecies, Element, Species
13from pymatgen.core.lattice import Lattice
14from pymatgen.core.structure import Structure
15from pymatgen.analysis.structure_matcher import StructureMatcher
16from pymatgen.electronic_structure.core import Magmom
17from pymatgen.symmetry.structure import SymmetrizedStructure
18from pymatgen.io.cif import CifBlock, CifParser, CifWriter
19from pymatgen.io.vasp.inputs import Poscar
20from pymatgen.util.testing import PymatgenTest
21
22try:
23    import pybtex
24except ImportError:
25    pybtex = None
26
27
28class CifBlockTest(PymatgenTest):
29    def test_to_string(self):
30        with open(self.TEST_FILES_DIR / "Graphite.cif") as f:
31            s = f.read()
32        c = CifBlock.from_string(s)
33        cif_str_2 = str(CifBlock.from_string(str(c)))
34        cif_str = """data_53781-ICSD
35_database_code_ICSD   53781
36_audit_creation_date   2003-04-01
37_audit_update_record   2013-02-01
38_chemical_name_systematic   Carbon
39_chemical_formula_structural   C
40_chemical_formula_sum   C1
41_chemical_name_structure_type   Graphite(2H)
42_chemical_name_mineral   'Graphite 2H'
43_exptl_crystal_density_diffrn   2.22
44_publ_section_title   'Structure of graphite'
45loop_
46 _citation_id
47 _citation_journal_full
48 _citation_year
49 _citation_journal_volume
50 _citation_page_first
51 _citation_page_last
52 _citation_journal_id_ASTM
53  primary  'Physical Review (1,1893-132,1963/141,1966-188,1969)'
54  1917  10  661  696  PHRVAO
55loop_
56 _publ_author_name
57  'Hull, A.W.'
58_cell_length_a   2.47
59_cell_length_b   2.47
60_cell_length_c   6.8
61_cell_angle_alpha   90.
62_cell_angle_beta   90.
63_cell_angle_gamma   120.
64_cell_volume   35.93
65_cell_formula_units_Z   4
66_symmetry_space_group_name_H-M   'P 63/m m c'
67_symmetry_Int_Tables_number   194
68loop_
69 _symmetry_equiv_pos_site_id
70 _symmetry_equiv_pos_as_xyz
71  1  'x, x-y, -z+1/2'
72  2  '-x+y, y, -z+1/2'
73  3  '-y, -x, -z+1/2'
74  4  '-x+y, -x, -z+1/2'
75  5  '-y, x-y, -z+1/2'
76  6  'x, y, -z+1/2'
77  7  '-x, -x+y, z+1/2'
78  8  'x-y, -y, z+1/2'
79  9  'y, x, z+1/2'
80  10  'x-y, x, z+1/2'
81  11  'y, -x+y, z+1/2'
82  12  '-x, -y, z+1/2'
83  13  '-x, -x+y, -z'
84  14  'x-y, -y, -z'
85  15  'y, x, -z'
86  16  'x-y, x, -z'
87  17  'y, -x+y, -z'
88  18  '-x, -y, -z'
89  19  'x, x-y, z'
90  20  '-x+y, y, z'
91  21  '-y, -x, z'
92  22  '-x+y, -x, z'
93  23  '-y, x-y, z'
94  24  'x, y, z'
95loop_
96 _atom_type_symbol
97 _atom_type_oxidation_number
98  C0+  0
99loop_
100 _atom_site_label
101 _atom_site_type_symbol
102 _atom_site_symmetry_multiplicity
103 _atom_site_Wyckoff_symbol
104 _atom_site_fract_x
105 _atom_site_fract_y
106 _atom_site_fract_z
107 _atom_site_B_iso_or_equiv
108 _atom_site_occupancy
109 _atom_site_attached_hydrogens
110  C1  C0+  2  b  0  0  0.25  .  1.  0
111  C2  C0+  2  c  0.3333  0.6667  0.25  .  1.  0"""
112        for l1, l2, l3 in zip(str(c).split("\n"), cif_str.split("\n"), cif_str_2.split("\n")):
113            self.assertEqual(l1.strip(), l2.strip())
114            self.assertEqual(l2.strip(), l3.strip())
115
116    def test_double_quotes_and_underscore_data(self):
117        cif_str = """data_test
118_symmetry_space_group_name_H-M   "P -3 m 1"
119_thing   '_annoying_data'"""
120        cb = CifBlock.from_string(cif_str)
121        self.assertEqual(cb["_symmetry_space_group_name_H-M"], "P -3 m 1")
122        self.assertEqual(cb["_thing"], "_annoying_data")
123        self.assertEqual(str(cb), cif_str.replace('"', "'"))
124
125    def test_double_quoted_data(self):
126        cif_str = """data_test
127_thing   ' '_annoying_data''
128_other   " "_more_annoying_data""
129_more   ' "even more" ' """
130        cb = CifBlock.from_string(cif_str)
131        self.assertEqual(cb["_thing"], " '_annoying_data'")
132        self.assertEqual(cb["_other"], ' "_more_annoying_data"')
133        self.assertEqual(cb["_more"], ' "even more" ')
134
135    def test_nested_fake_multiline_quotes(self):
136        cif_str = """data_test
137_thing
138;
139long quotes
140 ;
141 still in the quote
142 ;
143actually going to end now
144;"""
145        cb = CifBlock.from_string(cif_str)
146        self.assertEqual(
147            cb["_thing"],
148            " long quotes  ;  still in the quote" "  ; actually going to end now",
149        )
150
151    def test_long_loop(self):
152        data = {
153            "_stuff1": ["A" * 30] * 2,
154            "_stuff2": ["B" * 30] * 2,
155            "_stuff3": ["C" * 30] * 2,
156        }
157        loops = [["_stuff1", "_stuff2", "_stuff3"]]
158        cif_str = """data_test
159loop_
160 _stuff1
161 _stuff2
162 _stuff3
163  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
164  CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
165  AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
166  CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"""
167        self.assertEqual(str(CifBlock(data, loops, "test")), cif_str)
168
169
170class CifIOTest(PymatgenTest):
171    def test_CifParser(self):
172        parser = CifParser(self.TEST_FILES_DIR / "LiFePO4.cif")
173        for s in parser.get_structures(True):
174            self.assertEqual(s.formula, "Li4 Fe4 P4 O16", "Incorrectly parsed cif.")
175
176        parser = CifParser(self.TEST_FILES_DIR / "V2O3.cif")
177        for s in parser.get_structures(True):
178            self.assertEqual(s.formula, "V4 O6")
179
180        bibtex_str = """
181@article{cifref0,
182    author = "Andersson, G.",
183    title = "Studies on vanadium oxides. I. Phase analysis",
184    journal = "Acta Chemica Scandinavica (1-27,1973-42,1988)",
185    volume = "8",
186    year = "1954",
187    pages = "1599--1606"
188}
189        """
190        self.assertEqual(parser.get_bibtex_string().strip(), bibtex_str.strip())
191
192        parser = CifParser(self.TEST_FILES_DIR / "Li2O.cif")
193        prim = parser.get_structures(True)[0]
194        self.assertEqual(prim.formula, "Li2 O1")
195        conv = parser.get_structures(False)[0]
196        self.assertEqual(conv.formula, "Li8 O4")
197
198        # test for disordered structures
199        parser = CifParser(self.TEST_FILES_DIR / "Li10GeP2S12.cif")
200        for s in parser.get_structures(True):
201            self.assertEqual(s.formula, "Li20.2 Ge2.06 P3.94 S24", "Incorrectly parsed cif.")
202        cif_str = r"""#\#CIF1.1
203##########################################################################
204#               Crystallographic Information Format file
205#               Produced by PyCifRW module
206#
207#  This is a CIF file.  CIF has been adopted by the International
208#  Union of Crystallography as the standard for data archiving and
209#  transmission.
210#
211#  For information on this file format, follow the CIF links at
212#  http://www.iucr.org
213##########################################################################
214
215data_FePO4
216_symmetry_space_group_name_H-M          'P 1'
217_cell_length_a                          10.4117668699
218_cell_length_b                          6.06717187997
219_cell_length_c                          4.75948953998
220loop_ # sometimes this is in a loop (incorrectly)
221_cell_angle_alpha
22291.0
223_cell_angle_beta                        92.0
224_cell_angle_gamma                       93.0
225_chemical_name_systematic               'Generated by pymatgen'
226_symmetry_Int_Tables_number             1
227_chemical_formula_structural            FePO4
228_chemical_formula_sum                   'Fe4 P4 O16'
229_cell_volume                            300.65685512
230_cell_formula_units_Z                   4
231loop_
232  _symmetry_equiv_pos_site_id
233  _symmetry_equiv_pos_as_xyz
234   1  'x, y, z'
235
236loop_
237  _atom_site_type_symbol
238  _atom_site_label
239  _atom_site_symmetry_multiplicity
240  _atom_site_fract_x
241  _atom_site_fract_y
242  _atom_site_fract_z
243  _atom_site_attached_hydrogens
244  _atom_site_B_iso_or_equiv
245  _atom_site_occupancy
246    Fe  Fe1  1  0.218728  0.750000  0.474867  0  .  1
247    Fe  JJ2  1  0.281272  0.250000  0.974867  0  .  1
248    # there's a typo here, parser should read the symbol from the
249    # _atom_site_type_symbol
250    Fe  Fe3  1  0.718728  0.750000  0.025133  0  .  1
251    Fe  Fe4  1  0.781272  0.250000  0.525133  0  .  1
252    P  P5  1  0.094613  0.250000  0.418243  0  .  1
253    P  P6  1  0.405387  0.750000  0.918243  0  .  1
254    P  P7  1  0.594613  0.250000  0.081757  0  .  1
255    P  P8  1  0.905387  0.750000  0.581757  0  .  1
256    O  O9  1  0.043372  0.750000  0.707138  0  .  1
257    O  O10  1  0.096642  0.250000  0.741320  0  .  1
258    O  O11  1  0.165710  0.046072  0.285384  0  .  1
259    O  O12  1  0.165710  0.453928  0.285384  0  .  1
260    O  O13  1  0.334290  0.546072  0.785384  0  .  1
261    O  O14  1  0.334290  0.953928  0.785384  0  .  1
262    O  O15  1  0.403358  0.750000  0.241320  0  .  1
263    O  O16  1  0.456628  0.250000  0.207138  0  .  1
264    O  O17  1  0.543372  0.750000  0.792862  0  .  1
265    O  O18  1  0.596642  0.250000  0.758680  0  .  1
266    O  O19  1  0.665710  0.046072  0.214616  0  .  1
267    O  O20  1  0.665710  0.453928  0.214616  0  .  1
268    O  O21  1  0.834290  0.546072  0.714616  0  .  1
269    O  O22  1  0.834290  0.953928  0.714616  0  .  1
270    O  O23  1  0.903358  0.750000  0.258680  0  .  1
271    O  O24  1  0.956628  0.250000  0.292862  0  .  1
272
273"""
274        parser = CifParser.from_string(cif_str)
275        struct = parser.get_structures(primitive=False)[0]
276        self.assertEqual(struct.formula, "Fe4 P4 O16")
277        self.assertAlmostEqual(struct.lattice.a, 10.4117668699)
278        self.assertAlmostEqual(struct.lattice.b, 6.06717187997)
279        self.assertAlmostEqual(struct.lattice.c, 4.75948953998)
280        self.assertAlmostEqual(struct.lattice.alpha, 91)
281        self.assertAlmostEqual(struct.lattice.beta, 92)
282        self.assertAlmostEqual(struct.lattice.gamma, 93)
283
284        with warnings.catch_warnings():
285            warnings.simplefilter("ignore")
286            parser = CifParser(self.TEST_FILES_DIR / "srycoo.cif")
287        self.assertEqual(parser.get_structures()[0].formula, "Sr5.6 Y2.4 Co8 O21")
288
289        # Test with a decimal Xyz. This should parse as two atoms in
290        # conventional cell if it is correct, one if not.
291        parser = CifParser(self.TEST_FILES_DIR / "Fe.cif")
292        self.assertEqual(len(parser.get_structures(primitive=False)[0]), 2)
293        self.assertFalse(parser.has_errors)
294
295    def test_get_symmetrized_structure(self):
296        parser = CifParser(self.TEST_FILES_DIR / "Li2O.cif")
297        sym_structure = parser.get_structures(primitive=False, symmetrized=True)[0]
298        structure = parser.get_structures(primitive=False, symmetrized=False)[0]
299        self.assertIsInstance(sym_structure, SymmetrizedStructure)
300        self.assertEqual(structure, sym_structure)
301        self.assertEqual(sym_structure.equivalent_indices, [[0, 1, 2, 3], [4, 5, 6, 7, 8, 9, 10, 11]])
302
303    def test_site_symbol_preference(self):
304        parser = CifParser(self.TEST_FILES_DIR / "site_type_symbol_test.cif")
305        self.assertEqual(parser.get_structures()[0].formula, "Ge0.4 Sb0.4 Te1")
306
307    def test_implicit_hydrogen(self):
308        with warnings.catch_warnings():
309            warnings.simplefilter("ignore")
310            parser = CifParser(self.TEST_FILES_DIR / "Senegalite_implicit_hydrogen.cif")
311            for s in parser.get_structures():
312                self.assertEqual(s.formula, "Al8 P4 O32")
313                self.assertEqual(sum(s.site_properties["implicit_hydrogens"]), 20)
314            self.assertIn(
315                "Structure has implicit hydrogens defined, "
316                "parsed structure unlikely to be suitable for use "
317                "in calculations unless hydrogens added.",
318                parser.warnings,
319            )
320            parser = CifParser(self.TEST_FILES_DIR / "cif_implicit_hydrogens_cod_1011130.cif")
321            s = parser.get_structures()[0]
322            self.assertIn(
323                "Structure has implicit hydrogens defined, "
324                "parsed structure unlikely to be suitable for use "
325                "in calculations unless hydrogens added.",
326                parser.warnings,
327            )
328
329    def test_CifParserSpringerPauling(self):
330        with warnings.catch_warnings():
331            warnings.simplefilter("ignore")
332            # Below are 10 tests for CIFs from the Springer Materials/Pauling file DBs.
333
334            # Partial occupancy on sites, incorrect label, previously unparsable
335            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1928405.cif")
336            for s in parser.get_structures(True):
337                self.assertEqual(s.formula, "Er1 Mn3.888 Fe2.112 Sn6")
338            self.assertTrue(parser.has_errors)
339
340            # Partial occupancy on sites, previously parsed as an ordered structure
341            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1011081.cif")
342            for s in parser.get_structures(True):
343                self.assertEqual(s.formula, "Zr0.2 Nb0.8")
344            self.assertTrue(parser.has_errors)
345
346            # Partial occupancy on sites, incorrect label, previously unparsable
347            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1615854.cif")
348            for s in parser.get_structures(True):
349                self.assertEqual(s.formula, "Na2 Al2 Si6 O16")
350            self.assertTrue(parser.has_errors)
351
352            # Partial occupancy on sites, incorrect label, previously unparsable
353            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1622133.cif")
354            for s in parser.get_structures(True):
355                self.assertEqual(s.formula, "Ca0.184 Mg13.016 Fe2.8 Si16 O48")
356            self.assertTrue(parser.has_errors)
357
358            # Partial occupancy on sites, previously parsed as an ordered structure
359            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1908491.cif")
360            for s in parser.get_structures(True):
361                self.assertEqual(s.formula, "Mn0.48 Zn0.52 Ga2 Se4")
362            self.assertTrue(parser.has_errors)
363
364            # Partial occupancy on sites, incorrect label, previously unparsable
365            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1811457.cif")
366            for s in parser.get_structures(True):
367                self.assertEqual(s.formula, "Ba2 Mg0.6 Zr0.2 Ta1.2 O6")
368            self.assertTrue(parser.has_errors)
369
370            # Incomplete powder diffraction data, previously unparsable
371            # This CIF file contains the molecular species "NH3" which is
372            # parsed as "N" because the label is "N{x}" (x = 1,2,..) and the
373            # corresponding symbol is "NH3". Since, the label and symbol are switched
374            # in CIFs from Springer Materials/Pauling file DBs, CifParser parses the
375            # element as "Nh" (Nihonium).
376            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1002871.cif")
377            self.assertEqual(parser.get_structures(True)[0].formula, "Cu1 Br2 Nh6")
378            self.assertEqual(parser.get_structures(True)[1].formula, "Cu1 Br4 Nh6")
379            self.assertTrue(parser.has_errors)
380
381            # Incomplete powder diffraction data, previously unparsable
382            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1704003.cif")
383            for s in parser.get_structures():
384                self.assertEqual(s.formula, "Rb4 Mn2 F12")
385            self.assertTrue(parser.has_errors)
386
387            # Unparsable species 'OH/OH2', previously parsed as "O"
388            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1500382.cif")
389            for s in parser.get_structures():
390                self.assertEqual(s.formula, "Mg6 B2 O6 F1.764")
391            self.assertTrue(parser.has_errors)
392
393            # Unparsable species 'OH/OH2', previously parsed as "O"
394            parser = CifParser(self.TEST_FILES_DIR / "PF_sd_1601634.cif")
395            for s in parser.get_structures():
396                self.assertEqual(s.formula, "Zn1.29 Fe0.69 As2 Pb1.02 O8")
397
398    def test_CifParserCod(self):
399        """
400        Parsing problematic cif files from the COD database
401        """
402        with warnings.catch_warnings():
403            warnings.simplefilter("ignore")
404
405            # Symbol in capital letters
406            parser = CifParser(self.TEST_FILES_DIR / "Cod_2100513.cif")
407            for s in parser.get_structures(True):
408                self.assertEqual(s.formula, "Ca4 Nb2.0 Al2 O12")
409
410            # Label in capital letters
411            parser = CifParser(self.TEST_FILES_DIR / "Cod_4115344.cif")
412            for s in parser.get_structures(True):
413                self.assertEqual(s.formula, "Mo4 P2 H60 C60 I4 O4")
414
415    def test_parse_symbol(self):
416        """
417        Test the _parse_symbol function with several potentially
418        problematic examples of symbols and labels.
419        """
420
421        test_cases = {
422            "MgT": "Mg",
423            "MgT1": "Mg",
424            "H(46A)": "H",
425            "O(M)": "O",
426            "N(Am)": "N",
427            "H1N2a": "H",
428            "CO(1)": "Co",
429            "Wat1": "O",
430            "MgM2A": "Mg",
431            "CaX": "Ca",
432            "X1": "X",
433            "X": "X",
434            "OA1": "O",
435            "NaA2": "Na",
436            "O-H2": "O",
437            "OD2": "O",
438            "OW": "O",
439            "SiT": "Si",
440            "SiTet": "Si",
441            "Na-Int": "Na",
442            "CaD1": "Ca",
443            "KAm": "K",
444            "D+1": "D",
445            "D": "D",
446            "D1-": "D",
447            "D4": "D",
448            "D0": "D",
449            "NH": "Nh",
450            "NH2": "Nh",
451            "NH3": "Nh",
452            "SH": "S",
453        }
454
455        for e in Element:
456            name = e.name
457            test_cases[name] = name
458            if len(name) == 2:
459                test_cases[name.upper()] = name
460                test_cases[name.upper() + str(1)] = name
461                test_cases[name.upper() + "A"] = name
462            test_cases[name + str(1)] = name
463            test_cases[name + str(2)] = name
464            test_cases[name + str(3)] = name
465            test_cases[name + str(1) + "A"] = name
466
467        special = {"Hw": "H", "Ow": "O", "Wat": "O", "wat": "O", "OH": "", "OH2": ""}
468        test_cases.update(special)
469
470        with warnings.catch_warnings():
471            warnings.simplefilter("ignore")
472            parser = CifParser(self.TEST_FILES_DIR / "LiFePO4.cif")
473            for sym, expected_symbol in test_cases.items():
474                self.assertEqual(parser._parse_symbol(sym), expected_symbol)
475
476    def test_CifWriter(self):
477        filepath = self.TEST_FILES_DIR / "POSCAR"
478        poscar = Poscar.from_file(filepath)
479        writer = CifWriter(poscar.structure, symprec=0.01)
480        ans = """# generated using pymatgen
481data_FePO4
482_symmetry_space_group_name_H-M   Pnma
483_cell_length_a   10.41176687
484_cell_length_b   6.06717188
485_cell_length_c   4.75948954
486_cell_angle_alpha   90.00000000
487_cell_angle_beta   90.00000000
488_cell_angle_gamma   90.00000000
489_symmetry_Int_Tables_number   62
490_chemical_formula_structural   FePO4
491_chemical_formula_sum   'Fe4 P4 O16'
492_cell_volume   300.65685512
493_cell_formula_units_Z   4
494loop_
495 _symmetry_equiv_pos_site_id
496 _symmetry_equiv_pos_as_xyz
497  1  'x, y, z'
498  2  '-x, -y, -z'
499  3  '-x+1/2, -y, z+1/2'
500  4  'x+1/2, y, -z+1/2'
501  5  'x+1/2, -y+1/2, -z+1/2'
502  6  '-x+1/2, y+1/2, z+1/2'
503  7  '-x, y+1/2, -z'
504  8  'x, -y+1/2, z'
505loop_
506 _atom_site_type_symbol
507 _atom_site_label
508 _atom_site_symmetry_multiplicity
509 _atom_site_fract_x
510 _atom_site_fract_y
511 _atom_site_fract_z
512 _atom_site_occupancy
513  Fe  Fe0  4  0.21872822  0.75000000  0.47486711  1
514  P  P1  4  0.09461309  0.25000000  0.41824327  1
515  O  O2  8  0.16570974  0.04607233  0.28538394  1
516  O  O3  4  0.04337231  0.75000000  0.70713767  1
517  O  O4  4  0.09664244  0.25000000  0.74132035  1"""
518        for l1, l2 in zip(str(writer).split("\n"), ans.split("\n")):
519            self.assertEqual(l1.strip(), l2.strip())
520
521    def test_symmetrized(self):
522        filepath = self.TEST_FILES_DIR / "POSCAR"
523        poscar = Poscar.from_file(filepath, check_for_POTCAR=False)
524        writer = CifWriter(poscar.structure, symprec=0.1)
525        ans = """# generated using pymatgen
526data_FePO4
527_symmetry_space_group_name_H-M   Pnma
528_cell_length_a   10.41176687
529_cell_length_b   6.06717188
530_cell_length_c   4.75948954
531_cell_angle_alpha   90.00000000
532_cell_angle_beta   90.00000000
533_cell_angle_gamma   90.00000000
534_symmetry_Int_Tables_number   62
535_chemical_formula_structural   FePO4
536_chemical_formula_sum   'Fe4 P4 O16'
537_cell_volume   300.65685512
538_cell_formula_units_Z   4
539loop_
540 _symmetry_equiv_pos_site_id
541 _symmetry_equiv_pos_as_xyz
542  1  'x, y, z'
543  2  '-x, -y, -z'
544  3  '-x+1/2, -y, z+1/2'
545  4  'x+1/2, y, -z+1/2'
546  5  'x+1/2, -y+1/2, -z+1/2'
547  6  '-x+1/2, y+1/2, z+1/2'
548  7  '-x, y+1/2, -z'
549  8  'x, -y+1/2, z'
550loop_
551 _atom_site_type_symbol
552 _atom_site_label
553 _atom_site_symmetry_multiplicity
554 _atom_site_fract_x
555 _atom_site_fract_y
556 _atom_site_fract_z
557 _atom_site_occupancy
558  Fe  Fe1  4  0.218728  0.250000  0.525133  1
559  P  P2  4  0.094613  0.750000  0.581757  1
560  O  O3  8  0.165710  0.546072  0.714616  1
561  O  O4  4  0.043372  0.250000  0.292862  1
562  O  O5  4  0.096642  0.750000  0.258680  1"""
563
564        cif = CifParser.from_string(str(writer))
565        m = StructureMatcher()
566
567        self.assertTrue(m.fit(cif.get_structures()[0], poscar.structure))
568
569        # for l1, l2 in zip(str(writer).split("\n"), ans.split("\n")):
570        #     self.assertEqual(l1.strip(), l2.strip())
571
572        ans = """# generated using pymatgen
573data_LiFePO4
574_symmetry_space_group_name_H-M   Pnma
575_cell_length_a   10.41037000
576_cell_length_b   6.06577000
577_cell_length_c   4.74480000
578_cell_angle_alpha   90.00000000
579_cell_angle_beta   90.00000000
580_cell_angle_gamma   90.00000000
581_symmetry_Int_Tables_number   62
582_chemical_formula_structural   LiFePO4
583_chemical_formula_sum   'Li4 Fe4 P4 O16'
584_cell_volume   299.619458734
585_cell_formula_units_Z   4
586loop_
587 _symmetry_equiv_pos_site_id
588 _symmetry_equiv_pos_as_xyz
589  1  'x, y, z'
590  2  '-x, -y, -z'
591  3  '-x+1/2, -y, z+1/2'
592  4  'x+1/2, y, -z+1/2'
593  5  'x+1/2, -y+1/2, -z+1/2'
594  6  '-x+1/2, y+1/2, z+1/2'
595  7  '-x, y+1/2, -z'
596  8  'x, -y+1/2, z'
597loop_
598 _atom_site_type_symbol
599 _atom_site_label
600 _atom_site_symmetry_multiplicity
601 _atom_site_fract_x
602 _atom_site_fract_y
603 _atom_site_fract_z
604 _atom_site_occupancy
605  Li  Li1  4  0.000000  0.000000  0.000000  1.0
606  Fe  Fe2  4  0.218845  0.750000  0.474910  1.0
607  P  P3  4  0.094445  0.250000  0.417920  1.0
608  O  O4  8  0.165815  0.044060  0.286540  1.0
609  O  O5  4  0.043155  0.750000  0.708460  1.0
610  O  O6  4  0.096215  0.250000  0.741480  1.0
611"""
612        s = Structure.from_file(self.TEST_FILES_DIR / "LiFePO4.cif")
613        writer = CifWriter(s, symprec=0.1)
614        s2 = CifParser.from_string(str(writer)).get_structures()[0]
615
616        self.assertTrue(m.fit(s, s2))
617
618        s = self.get_structure("Li2O")
619        writer = CifWriter(s, symprec=0.1)
620        s2 = CifParser.from_string(str(writer)).get_structures()[0]
621        self.assertTrue(m.fit(s, s2))
622
623        # test angle tolerance.
624        s = Structure.from_file(self.TEST_FILES_DIR / "LiFePO4.cif")
625        writer = CifWriter(s, symprec=0.1, angle_tolerance=0)
626        d = list(writer.ciffile.data.values())[0]
627        self.assertEqual(d["_symmetry_Int_Tables_number"], 14)
628        s = Structure.from_file(self.TEST_FILES_DIR / "LiFePO4.cif")
629        writer = CifWriter(s, symprec=0.1, angle_tolerance=2)
630        d = list(writer.ciffile.data.values())[0]
631        self.assertEqual(d["_symmetry_Int_Tables_number"], 62)
632
633    def test_disordered(self):
634        si = Element("Si")
635        n = Element("N")
636        coords = []
637        coords.append(np.array([0, 0, 0]))
638        coords.append(np.array([0.75, 0.5, 0.75]))
639        lattice = Lattice(
640            np.array(
641                [
642                    [3.8401979337, 0.00, 0.00],
643                    [1.9200989668, 3.3257101909, 0.00],
644                    [0.00, -2.2171384943, 3.1355090603],
645                ]
646            )
647        )
648        struct = Structure(lattice, [si, {si: 0.5, n: 0.5}], coords)
649        writer = CifWriter(struct)
650        ans = """# generated using pymatgen
651data_Si1.5N0.5
652_symmetry_space_group_name_H-M   'P 1'
653_cell_length_a   3.84019793
654_cell_length_b   3.84019899
655_cell_length_c   3.84019793
656_cell_angle_alpha   119.99999086
657_cell_angle_beta   90.00000000
658_cell_angle_gamma   60.00000914
659_symmetry_Int_Tables_number   1
660_chemical_formula_structural   Si1.5N0.5
661_chemical_formula_sum   'Si1.5 N0.5'
662_cell_volume   40.04479464
663_cell_formula_units_Z   1
664loop_
665 _symmetry_equiv_pos_site_id
666 _symmetry_equiv_pos_as_xyz
667  1  'x, y, z'
668loop_
669 _atom_site_type_symbol
670 _atom_site_label
671 _atom_site_symmetry_multiplicity
672 _atom_site_fract_x
673 _atom_site_fract_y
674 _atom_site_fract_z
675 _atom_site_occupancy
676  Si  Si0  1  0.00000000  0.00000000  0.00000000  1
677  Si  Si1  1  0.75000000  0.50000000  0.75000000  0.5
678  N  N2  1  0.75000000  0.50000000  0.75000000  0.5"""
679
680        for l1, l2 in zip(str(writer).split("\n"), ans.split("\n")):
681            self.assertEqual(l1.strip(), l2.strip())
682
683    def test_cifwrite_without_refinement(self):
684        si2 = Structure.from_file(self.TEST_FILES_DIR / "abinit" / "si.cif")
685        writer = CifWriter(si2, symprec=1e-3, significant_figures=10, refine_struct=False)
686        s = str(writer)
687        assert "Fd-3m" in s
688        same_si2 = CifParser.from_string(s).get_structures()[0]
689        assert len(si2) == len(same_si2)
690
691    def test_specie_cifwriter(self):
692        si4 = Species("Si", 4)
693        si3 = Species("Si", 3)
694        n = DummySpecies("X", -3)
695        coords = []
696        coords.append(np.array([0.5, 0.5, 0.5]))
697        coords.append(np.array([0.75, 0.5, 0.75]))
698        coords.append(np.array([0, 0, 0]))
699        lattice = Lattice(
700            np.array(
701                [
702                    [3.8401979337, 0.00, 0.00],
703                    [1.9200989668, 3.3257101909, 0.00],
704                    [0.00, -2.2171384943, 3.1355090603],
705                ]
706            )
707        )
708        struct = Structure(lattice, [n, {si3: 0.5, n: 0.5}, si4], coords)
709        writer = CifWriter(struct)
710        ans = """# generated using pymatgen
711data_X1.5Si1.5
712_symmetry_space_group_name_H-M   'P 1'
713_cell_length_a   3.84019793
714_cell_length_b   3.84019899
715_cell_length_c   3.84019793
716_cell_angle_alpha   119.99999086
717_cell_angle_beta   90.00000000
718_cell_angle_gamma   60.00000914
719_symmetry_Int_Tables_number   1
720_chemical_formula_structural   X1.5Si1.5
721_chemical_formula_sum   'X1.5 Si1.5'
722_cell_volume   40.04479464
723_cell_formula_units_Z   1
724loop_
725 _symmetry_equiv_pos_site_id
726 _symmetry_equiv_pos_as_xyz
727  1  'x, y, z'
728loop_
729 _atom_type_symbol
730 _atom_type_oxidation_number
731  X3-  -3.0
732  Si3+  3.0
733  Si4+  4.0
734loop_
735 _atom_site_type_symbol
736 _atom_site_label
737 _atom_site_symmetry_multiplicity
738 _atom_site_fract_x
739 _atom_site_fract_y
740 _atom_site_fract_z
741 _atom_site_occupancy
742  X3-  X0  1  0.50000000  0.50000000  0.50000000  1
743  X3-  X1  1  0.75000000  0.50000000  0.75000000  0.5
744  Si3+  Si2  1  0.75000000  0.50000000  0.75000000  0.5
745  Si4+  Si3  1  0.00000000  0.00000000  0.00000000  1
746"""
747        for l1, l2 in zip(str(writer).split("\n"), ans.split("\n")):
748            self.assertEqual(l1.strip(), l2.strip())
749
750        # test that mixed valence works properly
751        s2 = Structure.from_str(ans, "cif")
752        self.assertEqual(struct.composition, s2.composition)
753
754    def test_primes(self):
755        with warnings.catch_warnings():
756            warnings.simplefilter("ignore")
757            parser = CifParser(self.TEST_FILES_DIR / "C26H16BeN2O2S2.cif")
758            for s in parser.get_structures(False):
759                self.assertEqual(s.composition, 8 * Composition("C26H16BeN2O2S2"))
760
761    def test_missing_atom_site_type_with_oxistates(self):
762        with warnings.catch_warnings():
763            warnings.simplefilter("ignore")
764            parser = CifParser(self.TEST_FILES_DIR / "P24Ru4H252C296S24N16.cif")
765            c = Composition({"S0+": 24, "Ru0+": 4, "H0+": 252, "C0+": 296, "N0+": 16, "P0+": 24})
766            for s in parser.get_structures(False):
767                self.assertEqual(s.composition, c)
768
769    def test_no_coords_or_species(self):
770        with warnings.catch_warnings():
771            warnings.simplefilter("ignore")
772            string = """#generated using pymatgen
773    data_Si1.5N1.5
774    _symmetry_space_group_name_H-M   'P 1'
775    _cell_length_a   3.84019793
776    _cell_length_b   3.84019899
777    _cell_length_c   3.84019793
778    _cell_angle_alpha   119.99999086
779    _cell_angle_beta   90.00000000
780    _cell_angle_gamma   60.00000914
781    _symmetry_Int_Tables_number   1
782    _chemical_formula_structural   Si1.5N1.5
783    _chemical_formula_sum   'Si1.5 N1.5'
784    _cell_volume   40.0447946443
785    _cell_formula_units_Z   0
786    loop_
787      _symmetry_equiv_pos_site_id
788      _symmetry_equiv_pos_as_xyz
789      1  'x, y, z'
790    loop_
791      _atom_type_symbol
792      _atom_type_oxidation_number
793       Si3+  3.0
794       Si4+  4.0
795       N3-  -3.0
796    loop_
797      _atom_site_type_symbol
798      _atom_site_label
799      _atom_site_symmetry_multiplicity
800      _atom_site_fract_x
801      _atom_site_fract_y
802      _atom_site_fract_z
803      _atom_site_occupancy
804      ? ? ? ? ? ? ?
805    """
806            parser = CifParser.from_string(string)
807            self.assertRaises(ValueError, parser.get_structures)
808
809    def test_get_lattice_from_lattice_type(self):
810        cif_structure = """#generated using pymatgen
811data_FePO4
812_symmetry_space_group_name_H-M   Pnma
813_cell_length_a   10.41176687
814_cell_length_b   6.06717188
815_cell_length_c   4.75948954
816_chemical_formula_structural   FePO4
817_chemical_formula_sum   'Fe4 P4 O16'
818_cell_volume   300.65685512
819_cell_formula_units_Z   4
820_symmetry_cell_setting Orthorhombic
821loop_
822  _symmetry_equiv_pos_site_id
823  _symmetry_equiv_pos_as_xyz
824   1  'x, y, z'
825loop_
826  _atom_site_type_symbol
827  _atom_site_label
828  _atom_site_symmetry_multiplicity
829  _atom_site_fract_x
830  _atom_site_fract_y
831  _atom_site_fract_z
832  _atom_site_occupancy
833  Fe  Fe1  1  0.218728  0.750000  0.474867  1
834  Fe  Fe2  1  0.281272  0.250000  0.974867  1
835  Fe  Fe3  1  0.718728  0.750000  0.025133  1
836  Fe  Fe4  1  0.781272  0.250000  0.525133  1
837  P  P5  1  0.094613  0.250000  0.418243  1
838  P  P6  1  0.405387  0.750000  0.918243  1
839  P  P7  1  0.594613  0.250000  0.081757  1
840  P  P8  1  0.905387  0.750000  0.581757  1
841  O  O9  1  0.043372  0.750000  0.707138  1
842  O  O10  1  0.096642  0.250000  0.741320  1
843  O  O11  1  0.165710  0.046072  0.285384  1
844  O  O12  1  0.165710  0.453928  0.285384  1
845  O  O13  1  0.334290  0.546072  0.785384  1
846  O  O14  1  0.334290  0.953928  0.785384  1
847  O  O15  1  0.403358  0.750000  0.241320  1
848  O  O16  1  0.456628  0.250000  0.207138  1
849  O  O17  1  0.543372  0.750000  0.792862  1
850  O  O18  1  0.596642  0.250000  0.758680  1
851  O  O19  1  0.665710  0.046072  0.214616  1
852  O  O20  1  0.665710  0.453928  0.214616  1
853  O  O21  1  0.834290  0.546072  0.714616  1
854  O  O22  1  0.834290  0.953928  0.714616  1
855  O  O23  1  0.903358  0.750000  0.258680  1
856  O  O24  1  0.956628  0.250000  0.292862  1
857
858"""
859        cp = CifParser.from_string(cif_structure)
860        s_test = cp.get_structures(False)[0]
861        filepath = self.TEST_FILES_DIR / "POSCAR"
862        poscar = Poscar.from_file(filepath)
863        s_ref = poscar.structure
864
865        sm = StructureMatcher(stol=0.05, ltol=0.01, angle_tol=0.1)
866        self.assertTrue(sm.fit(s_ref, s_test))
867
868    def test_empty(self):
869        # single line
870        cb = CifBlock.from_string("data_mwe\nloop_\n_tag\n ''")
871        self.assertEqual(cb.data["_tag"][0], "")
872
873        # multi line
874        cb = CifBlock.from_string("data_mwe\nloop_\n_tag\n;\n;")
875        self.assertEqual(cb.data["_tag"][0], "")
876
877        cb2 = CifBlock.from_string(str(cb))
878        self.assertEqual(cb, cb2)
879
880    def test_bad_cif(self):
881        with warnings.catch_warnings():
882            warnings.simplefilter("ignore")
883            f = self.TEST_FILES_DIR / "bad_occu.cif"
884            p = CifParser(f)
885            self.assertRaises(ValueError, p.get_structures)
886            p = CifParser(f, occupancy_tolerance=2)
887            s = p.get_structures()[0]
888            self.assertAlmostEqual(s[0].species["Al3+"], 0.5)
889
890    def test_one_line_symm(self):
891        with warnings.catch_warnings():
892            warnings.simplefilter("ignore")
893            f = self.TEST_FILES_DIR / "OneLineSymmP1.cif"
894            p = CifParser(f)
895            s = p.get_structures()[0]
896            self.assertEqual(s.formula, "Ga4 Pb2 O8")
897
898    def test_no_symmops(self):
899        with warnings.catch_warnings():
900            warnings.simplefilter("ignore")
901            f = self.TEST_FILES_DIR / "nosymm.cif"
902            p = CifParser(f)
903            s = p.get_structures()[0]
904            self.assertEqual(s.formula, "H96 C60 O8")
905
906    def test_dot_positions(self):
907        f = self.TEST_FILES_DIR / "ICSD59959.cif"
908        p = CifParser(f)
909        s = p.get_structures()[0]
910        self.assertEqual(s.formula, "K1 Mn1 F3")
911
912    def test_replacing_finite_precision_frac_coords(self):
913        f = self.TEST_FILES_DIR / "cif_finite_precision_frac_coord_error.cif"
914        with warnings.catch_warnings():
915            p = CifParser(f)
916            s = p.get_structures()[0]
917            self.assertEqual(str(s.composition), "N5+24")
918            self.assertIn(
919                "Some fractional co-ordinates rounded to ideal " "values to avoid issues with finite precision.",
920                p.warnings,
921            )
922
923    def test_empty_deque(self):
924        s = """data_1526655
925_journal_name_full
926_space_group_IT_number           227
927_symmetry_space_group_name_Hall  'F 4d 2 3 -1d'
928_symmetry_space_group_name_H-M   'F d -3 m :1'
929_cell_angle_alpha                90
930_cell_angle_beta                 90
931_cell_angle_gamma                90
932_cell_formula_units_Z            8
933_cell_length_a                   5.381
934_cell_length_b                   5.381
935_cell_length_c                   5.381
936_cell_volume                     155.808
937loop_
938_atom_site_label
939_atom_site_type_symbol
940_atom_site_fract_x
941_atom_site_fract_y
942_atom_site_fract_z
943_atom_site_occupancy
944_atom_site_U_iso_or_equiv
945Si1 Si 0 0 0 1 0.0
946_iucr_refine_fcf_details
947;
948data_symmetries
949loop_
950  _space_group_symop_id
951  _space_group_symop_operation_xyz
952  1  x,y,z
953  2  -x+1/2,y+1/2,-z+1/2
954  3  -x,-y,-z
955  4  x-1/2,-y-1/2,z-1/2
956;"""
957        p = CifParser.from_string(s)
958        self.assertEqual(p.get_structures()[0].formula, "Si1")
959        cif = """
960data_1526655
961_journal_name_full
962_space_group_IT_number           227
963_symmetry_space_group_name_Hall  'F 4d 2 3 -1d'
964_symmetry_space_group_name_H-M   'F d -3 m :1'
965_cell_angle_alpha                90
966_cell_angle_beta                 90
967_cell_angle_gamma                90
968_cell_formula_units_Z            8
969_cell_length_a                   5.381
970_cell_length_b                   5.381
971_cell_length_c                   5.381
972_cell_volume                     155.808
973_iucr_refine_fcf_details
974;
975data_symmetries
976Some arbitrary multiline string
977;
978loop_
979_atom_site_label
980_atom_site_type_symbol
981_atom_site_fract_x
982_atom_site_fract_y
983_atom_site_fract_z
984_atom_site_occupancy
985_atom_site_U_iso_or_equiv
986Si1 Si 0 0 0 1 0.0
987"""
988        p = CifParser.from_string(cif)
989        self.assertRaises(ValueError, p.get_structures)
990
991
992class MagCifTest(PymatgenTest):
993    def setUp(self):
994        warnings.filterwarnings("ignore")
995        self.mcif = CifParser(self.TEST_FILES_DIR / "magnetic.example.NiO.mcif")
996        self.mcif_ncl = CifParser(self.TEST_FILES_DIR / "magnetic.ncl.example.GdB4.mcif")
997        self.mcif_incom = CifParser(self.TEST_FILES_DIR / "magnetic.incommensurate.example.Cr.mcif")
998        self.mcif_disord = CifParser(self.TEST_FILES_DIR / "magnetic.disordered.example.CuMnO2.mcif")
999        self.mcif_ncl2 = CifParser(self.TEST_FILES_DIR / "Mn3Ge_IR2.mcif")
1000
1001    def tearDown(self):
1002        warnings.simplefilter("default")
1003
1004    def test_mcif_detection(self):
1005        self.assertTrue(self.mcif.feature_flags["magcif"])
1006        self.assertTrue(self.mcif_ncl.feature_flags["magcif"])
1007        self.assertTrue(self.mcif_incom.feature_flags["magcif"])
1008        self.assertTrue(self.mcif_disord.feature_flags["magcif"])
1009        self.assertFalse(self.mcif.feature_flags["magcif_incommensurate"])
1010        self.assertFalse(self.mcif_ncl.feature_flags["magcif_incommensurate"])
1011        self.assertTrue(self.mcif_incom.feature_flags["magcif_incommensurate"])
1012        self.assertFalse(self.mcif_disord.feature_flags["magcif_incommensurate"])
1013
1014    def test_get_structures(self):
1015        # incommensurate structures not currently supported
1016        self.assertRaises(NotImplementedError, self.mcif_incom.get_structures)
1017
1018        # disordered magnetic structures not currently supported
1019        self.assertRaises(NotImplementedError, self.mcif_disord.get_structures)
1020
1021        # taken from self.mcif_ncl, removing explicit magnetic symmops
1022        # so that MagneticSymmetryGroup() has to be invoked
1023        magcifstr = """
1024data_5yOhtAoR
1025
1026_space_group.magn_name_BNS     "P 4/m' b' m' "
1027_cell_length_a                 7.1316
1028_cell_length_b                 7.1316
1029_cell_length_c                 4.0505
1030_cell_angle_alpha              90.00
1031_cell_angle_beta               90.00
1032_cell_angle_gamma              90.00
1033
1034loop_
1035_atom_site_label
1036_atom_site_type_symbol
1037_atom_site_fract_x
1038_atom_site_fract_y
1039_atom_site_fract_z
1040_atom_site_occupancy
1041Gd1 Gd 0.31746 0.81746 0.00000 1
1042B1 B 0.00000 0.00000 0.20290 1
1043B2 B 0.17590 0.03800 0.50000 1
1044B3 B 0.08670 0.58670 0.50000 1
1045
1046loop_
1047_atom_site_moment_label
1048_atom_site_moment_crystalaxis_x
1049_atom_site_moment_crystalaxis_y
1050_atom_site_moment_crystalaxis_z
1051Gd1 5.05 5.05 0.0"""
1052
1053        s = self.mcif.get_structures(primitive=False)[0]
1054        self.assertEqual(s.formula, "Ni32 O32")
1055        self.assertTrue(Magmom.are_collinear(s.site_properties["magmom"]))
1056
1057        # example with non-collinear spin
1058        s_ncl = self.mcif_ncl.get_structures(primitive=False)[0]
1059        s_ncl_from_msg = CifParser.from_string(magcifstr).get_structures(primitive=False)[0]
1060        self.assertEqual(s_ncl.formula, "Gd4 B16")
1061        self.assertFalse(Magmom.are_collinear(s_ncl.site_properties["magmom"]))
1062
1063        self.assertTrue(s_ncl.matches(s_ncl_from_msg))
1064
1065    def test_write(self):
1066        cw_ref_string = """# generated using pymatgen
1067data_GdB4
1068_symmetry_space_group_name_H-M   'P 1'
1069_cell_length_a   7.13160000
1070_cell_length_b   7.13160000
1071_cell_length_c   4.05050000
1072_cell_angle_alpha   90.00000000
1073_cell_angle_beta   90.00000000
1074_cell_angle_gamma   90.00000000
1075_symmetry_Int_Tables_number   1
1076_chemical_formula_structural   GdB4
1077_chemical_formula_sum   'Gd4 B16'
1078_cell_volume   206.00729003
1079_cell_formula_units_Z   4
1080loop_
1081 _symmetry_equiv_pos_site_id
1082 _symmetry_equiv_pos_as_xyz
1083  1  'x, y, z'
1084loop_
1085 _atom_site_type_symbol
1086 _atom_site_label
1087 _atom_site_symmetry_multiplicity
1088 _atom_site_fract_x
1089 _atom_site_fract_y
1090 _atom_site_fract_z
1091 _atom_site_occupancy
1092  Gd  Gd0  1  0.31746000  0.81746000  0.00000000  1.0
1093  Gd  Gd1  1  0.18254000  0.31746000  0.00000000  1.0
1094  Gd  Gd2  1  0.81746000  0.68254000  0.00000000  1.0
1095  Gd  Gd3  1  0.68254000  0.18254000  0.00000000  1.0
1096  B  B4  1  0.00000000  0.00000000  0.20290000  1.0
1097  B  B5  1  0.50000000  0.50000000  0.79710000  1.0
1098  B  B6  1  0.00000000  0.00000000  0.79710000  1.0
1099  B  B7  1  0.50000000  0.50000000  0.20290000  1.0
1100  B  B8  1  0.17590000  0.03800000  0.50000000  1.0
1101  B  B9  1  0.96200000  0.17590000  0.50000000  1.0
1102  B  B10  1  0.03800000  0.82410000  0.50000000  1.0
1103  B  B11  1  0.67590000  0.46200000  0.50000000  1.0
1104  B  B12  1  0.32410000  0.53800000  0.50000000  1.0
1105  B  B13  1  0.82410000  0.96200000  0.50000000  1.0
1106  B  B14  1  0.53800000  0.67590000  0.50000000  1.0
1107  B  B15  1  0.46200000  0.32410000  0.50000000  1.0
1108  B  B16  1  0.08670000  0.58670000  0.50000000  1.0
1109  B  B17  1  0.41330000  0.08670000  0.50000000  1.0
1110  B  B18  1  0.58670000  0.91330000  0.50000000  1.0
1111  B  B19  1  0.91330000  0.41330000  0.50000000  1.0
1112loop_
1113 _atom_site_moment_label
1114 _atom_site_moment_crystalaxis_x
1115 _atom_site_moment_crystalaxis_y
1116 _atom_site_moment_crystalaxis_z
1117  Gd0  5.05000000  5.05000000  0.00000000
1118  Gd1  -5.05000000  5.05000000  0.00000000
1119  Gd2  5.05000000  -5.05000000  0.00000000
1120  Gd3  -5.05000000  -5.05000000  0.00000000
1121"""
1122        s_ncl = self.mcif_ncl.get_structures(primitive=False)[0]
1123
1124        cw = CifWriter(s_ncl, write_magmoms=True)
1125        self.assertEqual(cw.__str__(), cw_ref_string)
1126
1127        # from list-type magmoms
1128        list_magmoms = [list(m) for m in s_ncl.site_properties["magmom"]]
1129
1130        # float magmoms (magnitude only)
1131        float_magmoms = [float(m) for m in s_ncl.site_properties["magmom"]]
1132
1133        s_ncl.add_site_property("magmom", list_magmoms)
1134        cw = CifWriter(s_ncl, write_magmoms=True)
1135        self.assertEqual(cw.__str__(), cw_ref_string)
1136
1137        s_ncl.add_site_property("magmom", float_magmoms)
1138        cw = CifWriter(s_ncl, write_magmoms=True)
1139
1140        cw_ref_string_magnitudes = """# generated using pymatgen
1141data_GdB4
1142_symmetry_space_group_name_H-M   'P 1'
1143_cell_length_a   7.13160000
1144_cell_length_b   7.13160000
1145_cell_length_c   4.05050000
1146_cell_angle_alpha   90.00000000
1147_cell_angle_beta   90.00000000
1148_cell_angle_gamma   90.00000000
1149_symmetry_Int_Tables_number   1
1150_chemical_formula_structural   GdB4
1151_chemical_formula_sum   'Gd4 B16'
1152_cell_volume   206.00729003
1153_cell_formula_units_Z   4
1154loop_
1155 _symmetry_equiv_pos_site_id
1156 _symmetry_equiv_pos_as_xyz
1157  1  'x, y, z'
1158loop_
1159 _atom_site_type_symbol
1160 _atom_site_label
1161 _atom_site_symmetry_multiplicity
1162 _atom_site_fract_x
1163 _atom_site_fract_y
1164 _atom_site_fract_z
1165 _atom_site_occupancy
1166  Gd  Gd0  1  0.31746000  0.81746000  0.00000000  1.0
1167  Gd  Gd1  1  0.18254000  0.31746000  0.00000000  1.0
1168  Gd  Gd2  1  0.81746000  0.68254000  0.00000000  1.0
1169  Gd  Gd3  1  0.68254000  0.18254000  0.00000000  1.0
1170  B  B4  1  0.00000000  0.00000000  0.20290000  1.0
1171  B  B5  1  0.50000000  0.50000000  0.79710000  1.0
1172  B  B6  1  0.00000000  0.00000000  0.79710000  1.0
1173  B  B7  1  0.50000000  0.50000000  0.20290000  1.0
1174  B  B8  1  0.17590000  0.03800000  0.50000000  1.0
1175  B  B9  1  0.96200000  0.17590000  0.50000000  1.0
1176  B  B10  1  0.03800000  0.82410000  0.50000000  1.0
1177  B  B11  1  0.67590000  0.46200000  0.50000000  1.0
1178  B  B12  1  0.32410000  0.53800000  0.50000000  1.0
1179  B  B13  1  0.82410000  0.96200000  0.50000000  1.0
1180  B  B14  1  0.53800000  0.67590000  0.50000000  1.0
1181  B  B15  1  0.46200000  0.32410000  0.50000000  1.0
1182  B  B16  1  0.08670000  0.58670000  0.50000000  1.0
1183  B  B17  1  0.41330000  0.08670000  0.50000000  1.0
1184  B  B18  1  0.58670000  0.91330000  0.50000000  1.0
1185  B  B19  1  0.91330000  0.41330000  0.50000000  1.0
1186loop_
1187 _atom_site_moment_label
1188 _atom_site_moment_crystalaxis_x
1189 _atom_site_moment_crystalaxis_y
1190 _atom_site_moment_crystalaxis_z
1191  Gd0  0.00000000  0.00000000  7.14177849
1192  Gd1  0.00000000  0.00000000  7.14177849
1193  Gd2  0.00000000  0.00000000  -7.14177849
1194  Gd3  0.00000000  0.00000000  -7.14177849
1195"""
1196        self.assertEqual(cw.__str__().strip(), cw_ref_string_magnitudes.strip())
1197        # test we're getting correct magmoms in ncl case
1198        s_ncl2 = self.mcif_ncl2.get_structures()[0]
1199        list_magmoms = [list(m) for m in s_ncl2.site_properties["magmom"]]
1200        self.assertEqual(list_magmoms[0][0], 0.0)
1201        self.assertAlmostEqual(list_magmoms[0][1], 5.9160793408726366)
1202        self.assertAlmostEqual(list_magmoms[1][0], -5.1234749999999991)
1203        self.assertAlmostEqual(list_magmoms[1][1], 2.9580396704363183)
1204
1205        # test creating an structure without oxidation state doesn't raise errors
1206        s_manual = Structure(Lattice.cubic(4.2), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1207        s_manual.add_spin_by_site([1, -1])
1208        cw = CifWriter(s_manual, write_magmoms=True)
1209
1210        # check oxidation state
1211        cw_manual_oxi_string = """# generated using pymatgen
1212data_CsCl
1213_symmetry_space_group_name_H-M   'P 1'
1214_cell_length_a   4.20000000
1215_cell_length_b   4.20000000
1216_cell_length_c   4.20000000
1217_cell_angle_alpha   90.00000000
1218_cell_angle_beta   90.00000000
1219_cell_angle_gamma   90.00000000
1220_symmetry_Int_Tables_number   1
1221_chemical_formula_structural   CsCl
1222_chemical_formula_sum   'Cs1 Cl1'
1223_cell_volume   74.08800000
1224_cell_formula_units_Z   1
1225loop_
1226 _symmetry_equiv_pos_site_id
1227 _symmetry_equiv_pos_as_xyz
1228  1  'x, y, z'
1229loop_
1230 _atom_type_symbol
1231 _atom_type_oxidation_number
1232  Cs+  1.0
1233  Cl+  1.0
1234loop_
1235 _atom_site_type_symbol
1236 _atom_site_label
1237 _atom_site_symmetry_multiplicity
1238 _atom_site_fract_x
1239 _atom_site_fract_y
1240 _atom_site_fract_z
1241 _atom_site_occupancy
1242  Cs+  Cs0  1  0.00000000  0.00000000  0.00000000  1
1243  Cl+  Cl1  1  0.50000000  0.50000000  0.50000000  1
1244loop_
1245 _atom_site_moment_label
1246 _atom_site_moment_crystalaxis_x
1247 _atom_site_moment_crystalaxis_y
1248 _atom_site_moment_crystalaxis_z
1249"""
1250        s_manual.add_oxidation_state_by_site([1, 1])
1251        cw = CifWriter(s_manual, write_magmoms=True)
1252        self.assertEqual(cw.__str__(), cw_manual_oxi_string)
1253
1254    @unittest.skipIf(pybtex is None, "pybtex not present")
1255    def test_bibtex(self):
1256        ref_bibtex_string = """@article{cifref0,
1257    author = "Blanco, J.A.",
1258    journal = "PHYSICAL REVIEW B",
1259    volume = "73",
1260    year = "2006",
1261    pages = "?--?"
1262}
1263"""
1264        self.assertEqual(self.mcif_ncl.get_bibtex_string(), ref_bibtex_string)
1265
1266
1267if __name__ == "__main__":
1268    unittest.main()
1269