• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.66
/pymatgen/core/tests/test_structure.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
from __future__ import annotations
1✔
5

6
import json
1✔
7
import os
1✔
8
import random
1✔
9
import unittest
1✔
10
import warnings
1✔
11
from pathlib import Path
1✔
12
from shutil import which
1✔
13

14
import numpy as np
1✔
15
import pytest
1✔
16
from monty.json import MontyDecoder, MontyEncoder
1✔
17
from monty.tempfile import ScratchDir
1✔
18

19
from pymatgen.core.composition import Composition
1✔
20
from pymatgen.core.lattice import Lattice
1✔
21
from pymatgen.core.operations import SymmOp
1✔
22
from pymatgen.core.periodic_table import Element, Species
1✔
23
from pymatgen.core.structure import (
1✔
24
    IMolecule,
25
    IStructure,
26
    Molecule,
27
    PeriodicNeighbor,
28
    Structure,
29
    StructureError,
30
)
31
from pymatgen.electronic_structure.core import Magmom
1✔
32
from pymatgen.util.testing import PymatgenTest
1✔
33

34
enum_cmd = which("enum.x") or which("multienum.x")
1✔
35
mcsqs_cmd = which("mcsqs")
1✔
36

37
try:
1✔
38
    import m3gnet
1✔
39
except ImportError:
×
40
    m3gnet = None
×
41

42

43
class NeighborTest(PymatgenTest):
1✔
44
    def test_msonable(self):
1✔
45
        s = PymatgenTest.get_structure("Li2O")
1✔
46
        nn = s.get_neighbors(s[0], r=3)
1✔
47
        assert isinstance(nn[0], PeriodicNeighbor)
1✔
48
        str_ = json.dumps(nn, cls=MontyEncoder)
1✔
49
        nn = json.loads(str_, cls=MontyDecoder)
1✔
50
        assert isinstance(nn[0], PeriodicNeighbor)
1✔
51

52

53
class IStructureTest(PymatgenTest):
1✔
54
    def setUp(self):
1✔
55
        coords = [[0, 0, 0], [0.75, 0.5, 0.75]]
1✔
56
        self.lattice = Lattice(
1✔
57
            [
58
                [3.8401979337, 0.00, 0.00],
59
                [1.9200989668, 3.3257101909, 0.00],
60
                [0.00, -2.2171384943, 3.1355090603],
61
            ]
62
        )
63
        self.struct = IStructure(self.lattice, ["Si"] * 2, coords)
1✔
64
        assert len(self.struct) == 2, "Wrong number of sites in structure!"
1✔
65
        assert self.struct.is_ordered
1✔
66
        assert self.struct.ntypesp == 1
1✔
67
        coords = []
1✔
68
        coords.append([0, 0, 0])
1✔
69
        coords.append([0.0, 0, 0.0000001])
1✔
70
        with pytest.raises(StructureError):
1✔
71
            IStructure(
1✔
72
                self.lattice,
73
                ["Si"] * 2,
74
                coords,
75
                validate_proximity=True,
76
            )
77
        self.propertied_structure = IStructure(self.lattice, ["Si"] * 2, coords, site_properties={"magmom": [5, -5]})
1✔
78

79
        self.lattice_pbc = Lattice(
1✔
80
            [
81
                [3.8401979337, 0.00, 0.00],
82
                [1.9200989668, 3.3257101909, 0.00],
83
                [0.00, -2.2171384943, 3.1355090603],
84
            ],
85
            pbc=(True, True, False),
86
        )
87

88
    @unittest.skipIf(not (mcsqs_cmd and enum_cmd), "enumlib or mcsqs executable not present")
1✔
89
    def test_get_orderings(self):
1✔
90
        ordered = Structure.from_spacegroup("Im-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
×
91
        assert ordered.get_orderings()[0] == ordered
×
92
        disordered = Structure.from_spacegroup("Im-3m", Lattice.cubic(3), [Composition("Fe0.5Mn0.5")], [[0, 0, 0]])
×
93
        orderings = disordered.get_orderings()
×
94
        assert len(orderings) == 1
×
95
        super_cell = disordered * 2
×
96
        orderings = super_cell.get_orderings()
×
97
        assert len(orderings) == 59
×
98
        sqs = disordered.get_orderings(mode="sqs", scaling=[2, 2, 2])
×
99
        assert sqs[0].formula == "Mn8 Fe8"
×
100

101
        sqs = super_cell.get_orderings(mode="sqs")
×
102
        assert sqs[0].formula == "Mn8 Fe8"
×
103

104
    def test_as_dataframe(self):
1✔
105
        df = self.propertied_structure.as_dataframe()
1✔
106
        assert df.attrs["Reduced Formula"] == "Si"
1✔
107
        assert df.shape == (2, 8)
1✔
108

109
    def test_equal(self):
1✔
110
        struct = self.struct
1✔
111
        assert struct == struct
1✔
112
        assert struct == struct.copy()
1✔
113
        assert not struct == 2 * struct
1✔
114

115
        assert not struct == "a" * len(struct)  # GH-2584
1✔
116
        assert struct is not None
1✔
117
        assert not struct == 42  # GH-2587
1✔
118

119
        assert struct == Structure.from_dict(struct.as_dict())
1✔
120

121
        struct_2 = Structure.from_sites(struct)
1✔
122
        assert struct, struct_2
1✔
123
        struct_2.apply_strain(0.5)
1✔
124
        assert struct != struct_2
1✔
125

126
    def test_matches(self):
1✔
127
        ss = self.struct * 2
1✔
128
        assert ss.matches(self.struct)
1✔
129

130
    def test_bad_structure(self):
1✔
131
        coords = []
1✔
132
        coords.append([0, 0, 0])
1✔
133
        coords.append([0.75, 0.5, 0.75])
1✔
134
        coords.append([0.75, 0.5, 0.75])
1✔
135
        with pytest.raises(StructureError):
1✔
136
            IStructure(
1✔
137
                self.lattice,
138
                ["Si"] * 3,
139
                coords,
140
                validate_proximity=True,
141
            )
142
        # these shouldn't raise an error
143
        IStructure(self.lattice, ["Si"] * 2, coords[:2], True)
1✔
144
        IStructure(self.lattice, ["Si"], coords[:1], True)
1✔
145

146
    def test_volume_and_density(self):
1✔
147
        assert round(abs(self.struct.volume - 40.04), 2) == 0, "Volume wrong!"
1✔
148
        assert round(abs(self.struct.density - 2.33), 2) == 0, "Incorrect density"
1✔
149

150
    def test_specie_init(self):
1✔
151
        coords = []
1✔
152
        coords.append([0, 0, 0])
1✔
153
        coords.append([0.75, 0.5, 0.75])
1✔
154
        s = IStructure(self.lattice, [{Species("O", -2): 1.0}, {Species("Mg", 2): 0.8}], coords)
1✔
155
        assert s.composition.formula == "Mg0.8 O1"
1✔
156

157
    def test_get_sorted_structure(self):
1✔
158
        coords = []
1✔
159
        coords.append([0, 0, 0])
1✔
160
        coords.append([0.75, 0.5, 0.75])
1✔
161
        s = IStructure(self.lattice, ["O", "Li"], coords, site_properties={"charge": [-2, 1]})
1✔
162
        sorted_s = s.get_sorted_structure()
1✔
163
        assert sorted_s[0].species == Composition("Li")
1✔
164
        assert sorted_s[1].species == Composition("O")
1✔
165
        assert sorted_s[0].charge == 1
1✔
166
        assert sorted_s[1].charge == -2
1✔
167
        s = IStructure(
1✔
168
            self.lattice,
169
            ["Se", "C", "Se", "C"],
170
            [[0] * 3, [0.5] * 3, [0.25] * 3, [0.75] * 3],
171
        )
172
        assert [site.specie.symbol for site in s.get_sorted_structure()] == ["C", "C", "Se", "Se"]
1✔
173

174
    def test_get_space_group_data(self):
1✔
175
        assert self.struct.get_space_group_info() == ("Fd-3m", 227)
1✔
176

177
    def test_fractional_occupations(self):
1✔
178
        coords = []
1✔
179
        coords.append([0, 0, 0])
1✔
180
        coords.append([0.75, 0.5, 0.75])
1✔
181
        s = IStructure(self.lattice, [{"O": 1.0}, {"Mg": 0.8}], coords)
1✔
182
        assert s.composition.formula == "Mg0.8 O1"
1✔
183
        assert not s.is_ordered
1✔
184

185
    def test_get_distance(self):
1✔
186
        assert round(abs(self.struct.get_distance(0, 1) - 2.35), 2) == 0, "Distance calculated wrongly!"
1✔
187
        pt = [0.9, 0.9, 0.8]
1✔
188
        assert (
1✔
189
            round(abs(self.struct[0].distance_from_point(pt) - 1.50332963784), 2) == 0
190
        ), "Distance calculated wrongly!"
191

192
    def test_as_dict(self):
1✔
193
        si = Species("Si", 4)
1✔
194
        mn = Element("Mn")
1✔
195
        coords = []
1✔
196
        coords.append([0, 0, 0])
1✔
197
        coords.append([0.75, 0.5, 0.75])
1✔
198
        struct = IStructure(self.lattice, [{si: 0.5, mn: 0.5}, {si: 0.5}], coords)
1✔
199
        assert "lattice" in struct.as_dict()
1✔
200
        assert "sites" in struct.as_dict()
1✔
201
        d = self.propertied_structure.as_dict()
1✔
202
        assert d["sites"][0]["properties"]["magmom"] == 5
1✔
203
        coords = []
1✔
204
        coords.append([0, 0, 0])
1✔
205
        coords.append([0.75, 0.5, 0.75])
1✔
206
        s = IStructure(
1✔
207
            self.lattice,
208
            [
209
                {Species("O", -2, properties={"spin": 3}): 1.0},
210
                {Species("Mg", 2, properties={"spin": 2}): 0.8},
211
            ],
212
            coords,
213
            site_properties={"magmom": [5, -5]},
214
        )
215
        d = s.as_dict()
1✔
216
        assert d["sites"][0]["properties"]["magmom"] == 5
1✔
217
        assert d["sites"][0]["species"][0]["properties"]["spin"] == 3
1✔
218

219
        d = s.as_dict(0)
1✔
220
        assert "volume" not in d["lattice"]
1✔
221
        assert "xyz" not in d["sites"][0]
1✔
222

223
    def test_from_dict(self):
1✔
224
        d = self.propertied_structure.as_dict()
1✔
225
        s = IStructure.from_dict(d)
1✔
226
        assert s[0].magmom == 5
1✔
227
        d = self.propertied_structure.as_dict(0)
1✔
228
        s2 = IStructure.from_dict(d)
1✔
229
        assert s == s2
1✔
230

231
        d = {
1✔
232
            "lattice": {
233
                "a": 3.8401979337,
234
                "volume": 40.044794644251596,
235
                "c": 3.8401979337177736,
236
                "b": 3.840198994344244,
237
                "matrix": [
238
                    [3.8401979337, 0.0, 0.0],
239
                    [1.9200989668, 3.3257101909, 0.0],
240
                    [0.0, -2.2171384943, 3.1355090603],
241
                ],
242
                "alpha": 119.9999908639842,
243
                "beta": 90.0,
244
                "gamma": 60.000009137322195,
245
            },
246
            "sites": [
247
                {
248
                    "properties": {"magmom": 5},
249
                    "abc": [0.0, 0.0, 0.0],
250
                    "occu": 1.0,
251
                    "species": [
252
                        {
253
                            "occu": 1.0,
254
                            "oxidation_state": -2,
255
                            "properties": {"spin": 3},
256
                            "element": "O",
257
                        }
258
                    ],
259
                    "label": "O2-",
260
                    "xyz": [0.0, 0.0, 0.0],
261
                },
262
                {
263
                    "properties": {"magmom": -5},
264
                    "abc": [0.75, 0.5, 0.75],
265
                    "occu": 0.8,
266
                    "species": [
267
                        {
268
                            "occu": 0.8,
269
                            "oxidation_state": 2,
270
                            "properties": {"spin": 2},
271
                            "element": "Mg",
272
                        }
273
                    ],
274
                    "label": "Mg2+:0.800",
275
                    "xyz": [3.8401979336749994, 1.2247250003039056e-06, 2.351631795225],
276
                },
277
            ],
278
        }
279
        s = IStructure.from_dict(d)
1✔
280
        assert s[0].magmom == 5
1✔
281
        assert s[0].specie.spin == 3
1✔
282
        assert isinstance(s, IStructure)
1✔
283

284
    def test_site_properties(self):
1✔
285
        site_props = self.propertied_structure.site_properties
1✔
286
        assert site_props["magmom"] == [5, -5]
1✔
287
        assert self.propertied_structure[0].magmom == 5
1✔
288
        assert self.propertied_structure[1].magmom == -5
1✔
289

290
    def test_copy(self):
1✔
291
        new_struct = self.propertied_structure.copy(site_properties={"charge": [2, 3]})
1✔
292
        assert new_struct[0].magmom == 5
1✔
293
        assert new_struct[1].magmom == -5
1✔
294
        assert new_struct[0].charge == 2
1✔
295
        assert new_struct[1].charge == 3
1✔
296

297
        coords = []
1✔
298
        coords.append([0, 0, 0])
1✔
299
        coords.append([0.0, 0, 0.0000001])
1✔
300

301
        structure = IStructure(self.lattice, ["O", "Si"], coords, site_properties={"magmom": [5, -5]})
1✔
302

303
        new_struct = structure.copy(site_properties={"charge": [2, 3]}, sanitize=True)
1✔
304
        assert new_struct[0].magmom == -5
1✔
305
        assert new_struct[1].magmom == 5
1✔
306
        assert new_struct[0].charge == 3
1✔
307
        assert new_struct[1].charge == 2
1✔
308
        assert round(abs(new_struct.volume - structure.volume), 7) == 0
1✔
309

310
    def test_interpolate(self):
1✔
311
        coords = []
1✔
312
        coords.append([0, 0, 0])
1✔
313
        coords.append([0.75, 0.5, 0.75])
1✔
314
        struct = IStructure(self.lattice, ["Si"] * 2, coords)
1✔
315
        coords2 = []
1✔
316
        coords2.append([0, 0, 0])
1✔
317
        coords2.append([0.5, 0.5, 0.5])
1✔
318
        struct2 = IStructure(self.struct.lattice, ["Si"] * 2, coords2)
1✔
319
        int_s = struct.interpolate(struct2, 10)
1✔
320
        for s in int_s:
1✔
321
            assert s is not None, "Interpolation Failed!"
1✔
322
            assert int_s[0].lattice == s.lattice
1✔
323
        self.assertArrayEqual(int_s[1][1].frac_coords, [0.725, 0.5, 0.725])
1✔
324

325
        # test ximages
326
        int_s = struct.interpolate(struct2, nimages=np.linspace(0.0, 1.0, 3))
1✔
327
        for s in int_s:
1✔
328
            assert s is not None, "Interpolation Failed!"
1✔
329
            assert int_s[0].lattice == s.lattice
1✔
330
        self.assertArrayEqual(int_s[1][1].frac_coords, [0.625, 0.5, 0.625])
1✔
331

332
        badlattice = [[1, 0.00, 0.00], [0, 1, 0.00], [0.00, 0, 1]]
1✔
333
        struct2 = IStructure(badlattice, ["Si"] * 2, coords2)
1✔
334
        with pytest.raises(ValueError):
1✔
335
            struct.interpolate(struct2)
1✔
336

337
        coords2 = []
1✔
338
        coords2.append([0, 0, 0])
1✔
339
        coords2.append([0.5, 0.5, 0.5])
1✔
340
        struct2 = IStructure(self.struct.lattice, ["Si", "Fe"], coords2)
1✔
341
        with pytest.raises(ValueError):
1✔
342
            struct.interpolate(struct2)
1✔
343

344
        # Test autosort feature.
345
        s1 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
346
        s1.pop(0)
1✔
347
        s2 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
348
        s2.pop(2)
1✔
349
        random.shuffle(s2)
1✔
350

351
        for s in s1.interpolate(s2, autosort_tol=0.5):
1✔
352
            self.assertArrayAlmostEqual(s1[0].frac_coords, s[0].frac_coords)
1✔
353
            self.assertArrayAlmostEqual(s1[2].frac_coords, s[2].frac_coords)
1✔
354

355
        # Make sure autosort has no effect on simpler interpolations,
356
        # and with shuffled sites.
357
        s1 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
358
        s2 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
359
        s2[0] = "Fe", [0.01, 0.01, 0.01]
1✔
360
        random.shuffle(s2)
1✔
361

362
        for s in s1.interpolate(s2, autosort_tol=0.5):
1✔
363
            self.assertArrayAlmostEqual(s1[1].frac_coords, s[1].frac_coords)
1✔
364
            self.assertArrayAlmostEqual(s1[2].frac_coords, s[2].frac_coords)
1✔
365
            self.assertArrayAlmostEqual(s1[3].frac_coords, s[3].frac_coords)
1✔
366

367
        # Test non-hexagonal setting.
368
        lattice = Lattice.rhombohedral(4.0718, 89.459)
1✔
369
        species = [{"S": 1.0}, {"Ni": 1.0}]
1✔
370
        coordinate = [(0.252100, 0.252100, 0.252100), (0.500000, 0.244900, -0.244900)]
1✔
371
        s = Structure.from_spacegroup("R32:R", lattice, species, coordinate)
1✔
372
        assert s.formula == "Ni3 S2"
1✔
373

374
        # test pbc
375
        coords = [[0.85, 0.85, 0.85]]
1✔
376
        coords2 = [[0.25, 0.25, 0.25]]
1✔
377
        struct_pbc = IStructure(self.lattice_pbc, ["Si"], coords)
1✔
378
        struct2_pbc = IStructure(self.lattice_pbc, ["Si"], coords2)
1✔
379
        int_s_pbc = struct_pbc.interpolate(struct2_pbc, nimages=2)
1✔
380
        self.assertArrayAlmostEqual(int_s_pbc[1][0].frac_coords, [1.05, 1.05, 0.55])
1✔
381

382
    def test_interpolate_lattice(self):
1✔
383
        coords = []
1✔
384
        coords.append([0, 0, 0])
1✔
385
        coords.append([0.75, 0.5, 0.75])
1✔
386
        struct = IStructure(self.lattice, ["Si"] * 2, coords)
1✔
387
        coords2 = []
1✔
388
        coords2.append([0, 0, 0])
1✔
389
        coords2.append([0.5, 0.5, 0.5])
1✔
390
        l2 = Lattice.from_parameters(3, 4, 4, 100, 100, 70)
1✔
391
        struct2 = IStructure(l2, ["Si"] * 2, coords2)
1✔
392
        int_s = struct.interpolate(struct2, 2, interpolate_lattices=True)
1✔
393
        self.assertArrayAlmostEqual(struct.lattice.abc, int_s[0].lattice.abc)
1✔
394
        self.assertArrayAlmostEqual(struct.lattice.angles, int_s[0].lattice.angles)
1✔
395
        self.assertArrayAlmostEqual(struct2.lattice.abc, int_s[2].lattice.abc)
1✔
396
        self.assertArrayAlmostEqual(struct2.lattice.angles, int_s[2].lattice.angles)
1✔
397
        int_angles = [110.3976469, 94.5359731, 64.5165856]
1✔
398
        self.assertArrayAlmostEqual(int_angles, int_s[1].lattice.angles)
1✔
399

400
        # Assert that volume is monotonic
401
        assert struct2.lattice.volume >= int_s[1].lattice.volume
1✔
402
        assert int_s[1].lattice.volume >= struct.lattice.volume
1✔
403

404
    def test_interpolate_lattice_rotation(self):
1✔
405
        l1 = Lattice([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
1✔
406
        l2 = Lattice([[-1.01, 0, 0], [0, -1.01, 0], [0, 0, 1]])
1✔
407
        coords = [[0, 0, 0], [0.75, 0.5, 0.75]]
1✔
408
        struct1 = IStructure(l1, ["Si"] * 2, coords)
1✔
409
        struct2 = IStructure(l2, ["Si"] * 2, coords)
1✔
410
        int_s = struct1.interpolate(struct2, 2, interpolate_lattices=True)
1✔
411

412
        # Assert that volume is monotonic
413
        assert struct2.lattice.volume >= int_s[1].lattice.volume
1✔
414
        assert int_s[1].lattice.volume >= struct1.lattice.volume
1✔
415

416
    def test_get_primitive_structure(self):
1✔
417
        coords = [[0, 0, 0], [0.5, 0.5, 0], [0, 0.5, 0.5], [0.5, 0, 0.5]]
1✔
418
        fcc_ag = IStructure(Lattice.cubic(4.09), ["Ag"] * 4, coords)
1✔
419
        assert len(fcc_ag.get_primitive_structure()) == 1
1✔
420
        coords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
421
        bcc_li = IStructure(Lattice.cubic(4.09), ["Li"] * 2, coords)
1✔
422
        bcc_prim = bcc_li.get_primitive_structure()
1✔
423
        assert len(bcc_prim) == 1
1✔
424
        assert round(abs(bcc_prim.lattice.alpha - 109.47122), 3) == 0
1✔
425
        bcc_li = IStructure(Lattice.cubic(4.09), ["Li"] * 2, coords, site_properties={"magmom": [1, -1]})
1✔
426
        bcc_prim = bcc_li.get_primitive_structure()
1✔
427
        assert len(bcc_prim) == 1
1✔
428
        assert round(abs(bcc_prim.lattice.alpha - 109.47122), 3) == 0
1✔
429
        bcc_prim = bcc_li.get_primitive_structure(use_site_props=True)
1✔
430
        assert len(bcc_prim) == 2
1✔
431
        assert round(abs(bcc_prim.lattice.alpha - 90), 3) == 0
1✔
432

433
        coords = [[0] * 3, [0.5] * 3, [0.25] * 3, [0.26] * 3]
1✔
434
        s = IStructure(Lattice.cubic(4.09), ["Ag"] * 4, coords)
1✔
435
        assert len(s.get_primitive_structure()) == 4
1✔
436

437
    def test_primitive_cell_site_merging(self):
1✔
438
        l = Lattice.cubic(10)
1✔
439
        coords = [[0, 0, 0], [0, 0, 0.5], [0, 0, 0.26], [0, 0, 0.74]]
1✔
440
        sp = ["Ag", "Ag", "Be", "Be"]
1✔
441
        s = Structure(l, sp, coords)
1✔
442
        dm = s.get_primitive_structure().distance_matrix
1✔
443
        self.assertArrayAlmostEqual(dm, [[0, 2.5], [2.5, 0]])
1✔
444

445
    def test_primitive_on_large_supercell(self):
1✔
446
        coords = [[0, 0, 0], [0.5, 0.5, 0], [0, 0.5, 0.5], [0.5, 0, 0.5]]
1✔
447
        fcc_ag = Structure(Lattice.cubic(4.09), ["Ag"] * 4, coords)
1✔
448
        fcc_ag.make_supercell([2, 2, 2])
1✔
449
        fcc_ag_prim = fcc_ag.get_primitive_structure()
1✔
450
        assert len(fcc_ag_prim) == 1
1✔
451
        assert round(abs(fcc_ag_prim.volume - 17.10448225), 7) == 0
1✔
452

453
    def test_primitive_positions(self):
1✔
454
        coords = [[0, 0, 0], [0.3, 0.35, 0.45]]
1✔
455
        s = Structure(Lattice.from_parameters(1, 2, 3, 50, 66, 88), ["Ag"] * 2, coords)
1✔
456

457
        c = [[2, 0, 0], [1, 3, 0], [1, 1, 1]]
1✔
458

459
        for sc_matrix in [c]:
1✔
460
            sc = s.copy()
1✔
461
            sc.make_supercell(sc_matrix)
1✔
462
            prim = sc.get_primitive_structure(0.01)
1✔
463

464
            assert len(prim) == 2
1✔
465
            assert round(abs(prim.distance_matrix[0, 1] - 1.0203432356739286), 7) == 0
1✔
466

467
    def test_primitive_structure_volume_check(self):
1✔
468
        l = Lattice.tetragonal(10, 30)
1✔
469
        coords = [
1✔
470
            [0.5, 0.8, 0],
471
            [0.5, 0.2, 0],
472
            [0.5, 0.8, 0.333],
473
            [0.5, 0.5, 0.333],
474
            [0.5, 0.5, 0.666],
475
            [0.5, 0.2, 0.666],
476
        ]
477
        s = IStructure(l, ["Ag"] * 6, coords)
1✔
478
        sprim = s.get_primitive_structure(tolerance=0.1)
1✔
479
        assert len(sprim) == 6
1✔
480

481
    def test_get_miller_index(self):
1✔
482
        """Test for get miller index convenience method"""
483
        struct = Structure(
1✔
484
            [2.319, -4.01662582, 0.0, 2.319, 4.01662582, 0.0, 0.0, 0.0, 7.252],
485
            ["Sn", "Sn", "Sn"],
486
            [
487
                [2.319, 1.33887527, 6.3455],
488
                [1.1595, 0.66943764, 4.5325],
489
                [1.1595, 0.66943764, 0.9065],
490
            ],
491
            coords_are_cartesian=True,
492
        )
493
        hkl = struct.get_miller_index_from_site_indexes([0, 1, 2])
1✔
494
        assert hkl == (2, -1, 0)
1✔
495

496
    def test_get_all_neighbors_and_get_neighbors(self):
1✔
497
        s = self.struct
1✔
498
        nn = s.get_neighbors_in_shell(s[0].frac_coords, 2, 4, include_index=True, include_image=True)
1✔
499
        assert len(nn) == 47
1✔
500
        r = random.uniform(3, 6)
1✔
501
        all_nn = s.get_all_neighbors(r, True, True)
1✔
502
        for idx, site in enumerate(s):
1✔
503
            assert 4 == len(all_nn[idx][0])
1✔
504
            assert len(all_nn[idx]) == len(s.get_neighbors(site, r))
1✔
505

506
        for site, nns in zip(s, all_nn):
1✔
507
            for nn in nns:
1✔
508
                assert nn[0].is_periodic_image(s[nn[2]])
1✔
509
                d = sum((site.coords - nn[0].coords) ** 2) ** 0.5
1✔
510
                assert round(abs(d - nn[1]), 7) == 0
1✔
511

512
        s = Structure(Lattice.cubic(1), ["Li"], [[0, 0, 0]])
1✔
513
        s.make_supercell([2, 2, 2])
1✔
514
        assert sum(map(len, s.get_all_neighbors(3))) == 976
1✔
515

516
        all_nn = s.get_all_neighbors(0.05)
1✔
517
        assert [len(nn) for nn in all_nn] == [0] * len(s)
1✔
518

519
        # the following test is from issue #2226
520
        poscar = """POSCAR
1✔
521
 1.0000000000000000
522
     6.9082208665474800    0.0000000000000005    0.0000000000000011
523
    -0.0000000000000008   14.1745610988433715    0.0000000000000004
524
     0.0000000000000005   -0.0000000000000019   20.0189293405157045
525
C
526
 14
527
Direct
528
  1.2615805559557376  1.4846778919841908  0.3162565598606580
529
 -0.7615805559557360 -0.9846778919841882 -0.1837434401393425
530
  1.2615805559557380 -0.4846778919841886 -0.1837434401393425
531
 -0.7615805559557358  0.9846778919841882  0.3162565598606581
532
 -0.2615805559557363 -0.4846778919841892  0.6837434401393432
533
  0.7615805559557360  0.9846778919841881  0.1837434401393430
534
 -0.2653510291469959  0.5275483828607898  0.1211255106369795
535
  0.7653510291469972 -0.0275483828607906  0.6211255106369804
536
  1.2653510291469956  0.5275483828607898  0.3788744893630209
537
 -0.7653510291469972 -0.0275483828607906 -0.1211255106369797
538
  1.2653510291469956  0.4724516171392097 -0.1211255106369793
539
 -0.7653510291469972  0.0275483828607905  0.3788744893630209
540
 -0.2653510291469959  0.4724516171392097  0.6211255106369801
541
 -0.2705230397846415  1.4621722452479102  0.0625618775773844
542
"""
543
        s = Structure.from_str(poscar, fmt="poscar")
1✔
544
        site0 = s.sites[1]
1✔
545
        site1 = s.sites[9]
1✔
546
        neigh_sites = s.get_neighbors(site0, 2.0)
1✔
547
        assert len(neigh_sites) == 1
1✔
548
        neigh_sites = s.get_neighbors(site1, 2.0)
1✔
549
        assert len(neigh_sites) == 1
1✔
550

551
    def test_get_neighbor_list(self):
1✔
552
        s = self.struct
1✔
553
        c_indices1, c_indices2, c_offsets, c_distances = s.get_neighbor_list(3)
1✔
554
        p_indices1, p_indices2, p_offsets, p_distances = s._get_neighbor_list_py(3)
1✔
555
        self.assertArrayAlmostEqual(sorted(c_distances), sorted(p_distances))
1✔
556

557
    # @unittest.skipIf(not os.environ.get("CI"), "Only run this in CI tests.")
558
    # def test_get_all_neighbors_crosscheck_old(self):
559
    #     warnings.simplefilter("ignore")
560
    #     for i in range(100):
561
    #         alpha, beta = np.random.rand(2) * 90
562
    #         a, b, c = 3 + np.random.rand(3) * 5
563
    #         species = ["H"] * 5
564
    #         frac_coords = np.random.rand(5, 3)
565
    #         try:
566
    #             latt = Lattice.from_parameters(a, b, c, alpha, beta, 90)
567
    #             s = Structure.from_spacegroup("P1", latt,
568
    #                                           species, frac_coords)
569
    #             for nn_new, nn_old in zip(s.get_all_neighbors(4),
570
    #                                       s.get_all_neighbors_old(4)):
571
    #                 sites1 = [i[0] for i in nn_new]
572
    #                 sites2 = [i[0] for i in nn_old]
573
    #                 self.assertEqual(set(sites1), set(sites2))
574
    #             break
575
    #         except Exception as ex:
576
    #             pass
577
    #     else:
578
    #         raise ValueError("No valid structure tested.")
579
    #
580
    #     from pymatgen.electronic_structure.core import Spin
581
    #     d = {'@module': 'pymatgen.core.structure', '@class': 'Structure', 'charge': None, 'lattice': {
582
    #         'matrix': [[0.0, 0.0, 5.5333], [5.7461, 0.0, 3.518471486290303e-16],
583
    #                    [-4.692662837312786e-16, 7.6637, 4.692662837312786e-16]], 'a': 5.5333, 'b': 5.7461,
584
    #                    'c': 7.6637,
585
    #         'alpha': 90.0, 'beta': 90.0, 'gamma': 90.0, 'volume': 243.66653780778103}, 'sites': [
586
    #         {'species': [{'element': 'Mn', 'oxidation_state': 0, 'properties': {'spin': Spin.down}, 'occu': 1}],
587
    #          'abc': [0.0, 0.5, 0.5], 'xyz': [2.8730499999999997, 3.83185, 4.1055671618015446e-16],
588
    #          'label': 'Mn0+,spin=-1',
589
    #          'properties': {}},
590
    #         {'species': [{'element': 'Mn', 'oxidation_state': None, 'occu': 1.0}],
591
    #          'abc': [1.232595164407831e-32, 0.5, 0.5],
592
    #          'xyz': [2.8730499999999997, 3.83185, 4.105567161801545e-16], 'label': 'Mn', 'properties': {}}]}
593
    #     struct = Structure.from_dict(d)
594
    #     self.assertEqual(set([i[0] for i in struct.get_neighbors(struct[0], 0.05)]),
595
    #                      set([i[0] for i in struct.get_neighbors_old(struct[0], 0.05)]))
596
    #
597
    #     warnings.simplefilter("default")
598

599
    def test_get_symmetric_neighbor_list(self):
1✔
600
        # tetragonal group with all bonds related by symmetry
601
        s = Structure.from_spacegroup(100, [[1, 0, 0], [0, 1, 0], [0, 0, 2]], ["Fe"], [[0.0, 0.0, 0.0]])
1✔
602
        c_indices, p_indices, offsets, distances, s_indices, symops = s.get_symmetric_neighbor_list(0.8, sg=100)
1✔
603
        assert len(np.unique(s_indices)) == 1
1✔
604
        assert s_indices[0] == 0
1✔
605
        assert (~np.isnan(s_indices)).all()
1✔
606
        assert (symops[0].affine_matrix == np.eye(4)).all()
1✔
607
        # now more complicated example with bonds of same length but with different symmetry
608
        s2 = Structure.from_spacegroup(198, [[8.908, 0, 0], [0, 8.908, 0], [0, 0, 8.908]], ["Cu"], [[0.0, 0.0, 0.0]])
1✔
609
        c_indices2, p_indices2, offsets2, distances2, s_indices2, symops2 = s2.get_symmetric_neighbor_list(7, sg=198)
1✔
610
        assert len(np.unique(s_indices2)) == 2
1✔
611
        assert len(s_indices2) == 48
1✔
612
        assert len(s_indices2[s_indices2 == 0]) == len(s_indices2[s_indices2 == 1])
1✔
613
        assert s_indices2[0] == 0
1✔
614
        assert s_indices2[24] == 1
1✔
615
        assert np.isclose(distances2[0], distances2[24])
1✔
616
        assert (symops2[0].affine_matrix == np.eye(4)).all()
1✔
617
        assert (symops2[24].affine_matrix == np.eye(4)).all()
1✔
618
        from_a2 = s2[c_indices2[0]].frac_coords
1✔
619
        to_a2 = s2[p_indices2[0]].frac_coords
1✔
620
        r_a2 = offsets2[0]
1✔
621
        from_b2 = s2[c_indices2[1]].frac_coords
1✔
622
        to_b2 = s2[p_indices2[1]].frac_coords
1✔
623
        r_b2 = offsets2[1]
1✔
624
        assert symops2[1].are_symmetrically_related_vectors(from_a2, to_a2, r_a2, from_b2, to_b2, r_b2)
1✔
625
        assert symops2[1].are_symmetrically_related_vectors(from_b2, to_b2, r_b2, from_a2, to_a2, r_a2)
1✔
626
        c_indices3, p_indices3, offsets3, distances3, s_indices3, symops3 = s2.get_symmetric_neighbor_list(
1✔
627
            7, sg=198, unique=True
628
        )
629
        assert (np.sort(np.array([c_indices3, p_indices3]).flatten()) == np.sort(c_indices2)).all()
1✔
630
        assert (np.sort(np.array([c_indices3, p_indices3]).flatten()) == np.sort(p_indices2)).all()
1✔
631

632
    def test_get_all_neighbors_outside_cell(self):
1✔
633
        s = Structure(
1✔
634
            Lattice.cubic(2),
635
            ["Li", "Li", "Li", "Si"],
636
            [[3.1] * 3, [0.11] * 3, [-1.91] * 3, [0.5] * 3],
637
        )
638
        all_nn = s.get_all_neighbors(0.2, True)
1✔
639
        for site, nns in zip(s, all_nn):
1✔
640
            for nn in nns:
1✔
641
                assert nn[0].is_periodic_image(s[nn[2]])
1✔
642
                d = sum((site.coords - nn[0].coords) ** 2) ** 0.5
1✔
643
                assert round(abs(d - nn[1]), 7) == 0
1✔
644
        assert list(map(len, all_nn)) == [2, 2, 2, 0]
1✔
645

646
    def test_get_all_neighbors_small_cutoff(self):
1✔
647
        s = Structure(
1✔
648
            Lattice.cubic(2),
649
            ["Li", "Li", "Li", "Si"],
650
            [[3.1] * 3, [0.11] * 3, [-1.91] * 3, [0.5] * 3],
651
        )
652
        all_nn = s.get_all_neighbors(1e-5, True)
1✔
653
        assert len(all_nn) == len(s)
1✔
654
        assert [] == all_nn[0]
1✔
655

656
        all_nn = s.get_all_neighbors(0, True)
1✔
657
        assert len(all_nn) == len(s)
1✔
658
        assert [] == all_nn[0]
1✔
659

660
    def test_coincide_sites(self):
1✔
661
        s = Structure(
1✔
662
            Lattice.cubic(5),
663
            ["Li", "Li", "Li"],
664
            [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [3, 3, 3]],
665
            coords_are_cartesian=True,
666
        )
667
        all_nn = s.get_all_neighbors(1e-5, True)
1✔
668
        assert [len(i) for i in all_nn] == [0, 0, 0]
1✔
669

670
    def test_get_all_neighbors_equal(self):
1✔
671
        with pytest.warns(FutureWarning, match="get_all_neighbors_old is deprecated"):
1✔
672
            s = Structure(
1✔
673
                Lattice.cubic(2),
674
                ["Li", "Li", "Li", "Si"],
675
                [[3.1] * 3, [0.11] * 3, [-1.91] * 3, [0.5] * 3],
676
            )
677
            nn_traditional = s.get_all_neighbors_old(4, include_index=True, include_image=True, include_site=True)
1✔
678
            nn_cell_lists = s.get_all_neighbors(4, include_index=True, include_image=True)
1✔
679

680
            for i in range(4):
1✔
681
                assert len(nn_traditional[i]) == len(nn_cell_lists[i])
1✔
682
                assert (
1✔
683
                    np.linalg.norm(
684
                        np.array(sorted(j[1] for j in nn_traditional[i]))
685
                        - np.array(sorted(j[1] for j in nn_cell_lists[i]))
686
                    )
687
                    < 1e-3
688
                )
689

690
    def test_get_dist_matrix(self):
1✔
691
        ans = [[0.0, 2.3516318], [2.3516318, 0.0]]
1✔
692
        self.assertArrayAlmostEqual(self.struct.distance_matrix, ans)
1✔
693

694
    def test_to_from_file_string(self):
1✔
695
        with ScratchDir("."):
1✔
696
            for fmt in ["cif", "json", "poscar", "cssr"]:
1✔
697
                s = self.struct.to(fmt=fmt)
1✔
698
                assert s is not None
1✔
699
                ss = IStructure.from_str(s, fmt=fmt)
1✔
700
                self.assertArrayAlmostEqual(ss.lattice.parameters, self.struct.lattice.parameters, decimal=5)
1✔
701
                self.assertArrayAlmostEqual(ss.frac_coords, self.struct.frac_coords)
1✔
702
                assert isinstance(ss, IStructure)
1✔
703

704
            assert "Fd-3m" in self.struct.to(fmt="CIF", symprec=0.1)
1✔
705

706
            self.struct.to(filename="POSCAR.testing")
1✔
707
            assert os.path.exists("POSCAR.testing")
1✔
708

709
            self.struct.to(filename="Si_testing.yaml")
1✔
710
            assert os.path.exists("Si_testing.yaml")
1✔
711
            s = Structure.from_file("Si_testing.yaml")
1✔
712
            assert s == self.struct
1✔
713
            # Test Path support.
714
            s = Structure.from_file(Path("Si_testing.yaml"))
1✔
715
            assert s == self.struct
1✔
716

717
            # Test .yml extension works too.
718
            os.replace("Si_testing.yaml", "Si_testing.yml")
1✔
719
            s = Structure.from_file("Si_testing.yml")
1✔
720
            assert s == self.struct
1✔
721

722
            with pytest.raises(ValueError):
1✔
723
                self.struct.to(filename="whatever")
1✔
724
            with pytest.raises(ValueError):
1✔
725
                self.struct.to(fmt="badformat")
1✔
726

727
            self.struct.to(filename="POSCAR.testing.gz")
1✔
728
            s = Structure.from_file("POSCAR.testing.gz")
1✔
729
            assert s == self.struct
1✔
730

731
    def test_pbc(self):
1✔
732
        self.assertArrayEqual(self.struct.pbc, (True, True, True))
1✔
733
        assert self.struct.is_3d_periodic
1✔
734
        struct_pbc = Structure(self.lattice_pbc, ["Si"] * 2, self.struct.frac_coords)
1✔
735
        self.assertArrayEqual(struct_pbc.pbc, (True, True, False))
1✔
736
        assert not struct_pbc.is_3d_periodic
1✔
737

738

739
class StructureTest(PymatgenTest):
1✔
740
    def setUp(self):
1✔
741
        coords = []
1✔
742
        coords.append([0, 0, 0])
1✔
743
        coords.append([0.75, 0.5, 0.75])
1✔
744
        lattice = Lattice(
1✔
745
            [
746
                [3.8401979337, 0.00, 0.00],
747
                [1.9200989668, 3.3257101909, 0.00],
748
                [0.00, -2.2171384943, 3.1355090603],
749
            ]
750
        )
751
        self.structure = Structure(lattice, ["Si", "Si"], coords)
1✔
752

753
    def test_mutable_sequence_methods(self):
1✔
754
        s = self.structure
1✔
755
        s[0] = "Fe"
1✔
756
        assert s.formula == "Fe1 Si1"
1✔
757
        s[0] = "Fe", [0.5, 0.5, 0.5]
1✔
758
        assert s.formula == "Fe1 Si1"
1✔
759
        self.assertArrayAlmostEqual(s[0].frac_coords, [0.5, 0.5, 0.5])
1✔
760
        s.reverse()
1✔
761
        assert s[0].specie == Element("Si")
1✔
762
        self.assertArrayAlmostEqual(s[0].frac_coords, [0.75, 0.5, 0.75])
1✔
763
        s[0] = {"Mn": 0.5}
1✔
764
        assert s.formula == "Mn0.5 Fe1"
1✔
765
        del s[1]
1✔
766
        assert s.formula == "Mn0.5"
1✔
767
        s[0] = "Fe", [0.9, 0.9, 0.9], {"magmom": 5}
1✔
768
        assert s.formula == "Fe1"
1✔
769
        assert s[0].magmom == 5
1✔
770

771
        # Test atomic replacement.
772
        s["Fe"] = "Mn"
1✔
773
        assert s.formula == "Mn1"
1✔
774

775
        # Test slice replacement.
776
        s = PymatgenTest.get_structure("Li2O")
1✔
777
        s[0:2] = "S"
1✔
778
        assert s.formula == "Li1 S2"
1✔
779

780
    def test_non_hash(self):
1✔
781
        with pytest.raises(TypeError):
1✔
782
            dict([(self.structure, 1)])
1✔
783

784
    def test_sort(self):
1✔
785
        s = self.structure
1✔
786
        s[0] = "F"
1✔
787
        s.sort()
1✔
788
        assert s[0].species_string == "Si"
1✔
789
        assert s[1].species_string == "F"
1✔
790
        s.sort(key=lambda site: site.species_string)
1✔
791
        assert s[0].species_string == "F"
1✔
792
        assert s[1].species_string == "Si"
1✔
793
        s.sort(key=lambda site: site.species_string, reverse=True)
1✔
794
        assert s[0].species_string == "Si"
1✔
795
        assert s[1].species_string == "F"
1✔
796

797
    def test_append_insert_remove_replace_substitute(self):
1✔
798
        s = self.structure
1✔
799
        s.insert(1, "O", [0.5, 0.5, 0.5])
1✔
800
        assert s.formula == "Si2 O1"
1✔
801
        assert s.ntypesp == 2
1✔
802
        assert s.symbol_set == ("O", "Si")
1✔
803
        assert s.indices_from_symbol("Si") == (0, 2)
1✔
804
        assert s.indices_from_symbol("O") == (1,)
1✔
805
        del s[2]
1✔
806
        assert s.formula == "Si1 O1"
1✔
807
        assert s.indices_from_symbol("Si") == (0,)
1✔
808
        assert s.indices_from_symbol("O") == (1,)
1✔
809
        s.append("N", [0.25, 0.25, 0.25])
1✔
810
        assert s.formula == "Si1 N1 O1"
1✔
811
        assert s.ntypesp == 3
1✔
812
        assert s.symbol_set == ("N", "O", "Si")
1✔
813
        assert s.indices_from_symbol("Si") == (0,)
1✔
814
        assert s.indices_from_symbol("O") == (1,)
1✔
815
        assert s.indices_from_symbol("N") == (2,)
1✔
816
        s[0] = "Ge"
1✔
817
        assert s.formula == "Ge1 N1 O1"
1✔
818
        assert s.symbol_set == ("Ge", "N", "O")
1✔
819
        s.replace_species({"Ge": "Si"})
1✔
820
        assert s.formula == "Si1 N1 O1"
1✔
821
        assert s.ntypesp == 3
1✔
822

823
        s.replace_species({"Si": {"Ge": 0.5, "Si": 0.5}})
1✔
824
        assert s.formula == "Si0.5 Ge0.5 N1 O1"
1✔
825
        # this should change the .5Si .5Ge sites to .75Si .25Ge
826
        s.replace_species({"Ge": {"Ge": 0.5, "Si": 0.5}})
1✔
827
        assert s.formula == "Si0.75 Ge0.25 N1 O1"
1✔
828

829
        assert s.ntypesp == 4
1✔
830

831
        s.replace_species({"Ge": "Si"})
1✔
832
        s.substitute(1, "hydroxyl")
1✔
833
        assert s.formula == "Si1 H1 N1 O1"
1✔
834
        assert s.symbol_set == ("H", "N", "O", "Si")
1✔
835
        # Distance between O and H
836
        assert round(abs(s.get_distance(2, 3) - 0.96), 7) == 0
1✔
837
        # Distance between Si and H
838
        assert round(abs(s.get_distance(0, 3) - 2.09840889), 7) == 0
1✔
839

840
        s.remove_species(["H"])
1✔
841
        assert s.formula == "Si1 N1 O1"
1✔
842

843
        s.remove_sites([1, 2])
1✔
844
        assert s.formula == "Si1"
1✔
845

846
    def test_add_remove_site_property(self):
1✔
847
        s = self.structure
1✔
848
        s.add_site_property("charge", [4.1, -5])
1✔
849
        assert s[0].charge == 4.1
1✔
850
        assert s[1].charge == -5
1✔
851
        s.add_site_property("magmom", [3, 2])
1✔
852
        assert s[0].charge == 4.1
1✔
853
        assert s[0].magmom == 3
1✔
854
        s.remove_site_property("magmom")
1✔
855
        with pytest.raises(AttributeError):
1✔
856
            s[0].magmom
1✔
857

858
    def test_propertied_structure(self):
1✔
859
        # Make sure that site properties are set to None for missing values.
860
        s = self.structure
1✔
861
        s.add_site_property("charge", [4.1, -5])
1✔
862
        s.append("Li", [0.3, 0.3, 0.3])
1✔
863
        assert len(s.site_properties["charge"]) == 3
1✔
864

865
    def test_perturb(self):
1✔
866
        d = 0.1
1✔
867
        pre_perturbation_sites = self.structure.copy()
1✔
868
        self.structure.perturb(distance=d)
1✔
869
        post_perturbation_sites = self.structure.sites
1✔
870

871
        for i, x in enumerate(pre_perturbation_sites):
1✔
872
            assert round(abs(x.distance(post_perturbation_sites[i]) - d), 3) == 0, "Bad perturbation distance"
1✔
873

874
        structure2 = pre_perturbation_sites.copy()
1✔
875
        structure2.perturb(distance=d, min_distance=0)
1✔
876
        post_perturbation_sites2 = structure2.sites
1✔
877

878
        for i, x in enumerate(pre_perturbation_sites):
1✔
879
            assert x.distance(post_perturbation_sites2[i]) <= d
1✔
880
            assert x.distance(post_perturbation_sites2[i]) >= 0
1✔
881

882
    def test_add_oxidation_states(self):
1✔
883
        oxidation_states = {"Si": -4}
1✔
884
        self.structure.add_oxidation_state_by_element(oxidation_states)
1✔
885
        for site in self.structure:
1✔
886
            for k in site.species:
1✔
887
                assert k.oxi_state == oxidation_states[k.symbol], "Wrong oxidation state assigned!"
1✔
888
        oxidation_states = {"Fe": 2}
1✔
889
        with pytest.raises(ValueError):
1✔
890
            self.structure.add_oxidation_state_by_element(oxidation_states)
1✔
891
        self.structure.add_oxidation_state_by_site([2, -4])
1✔
892
        assert self.structure[0].specie.oxi_state == 2
1✔
893
        with pytest.raises(ValueError):
1✔
894
            self.structure.add_oxidation_state_by_site([1])
1✔
895

896
    def test_remove_oxidation_states(self):
1✔
897
        co_elem = Element("Co")
1✔
898
        o_elem = Element("O")
1✔
899
        co_specie = Species("Co", 2)
1✔
900
        o_specie = Species("O", -2)
1✔
901
        coords = []
1✔
902
        coords.append([0, 0, 0])
1✔
903
        coords.append([0.75, 0.5, 0.75])
1✔
904
        lattice = Lattice.cubic(10)
1✔
905
        s_elem = Structure(lattice, [co_elem, o_elem], coords)
1✔
906
        s_specie = Structure(lattice, [co_specie, o_specie], coords)
1✔
907
        s_specie.remove_oxidation_states()
1✔
908
        assert s_elem == s_specie, "Oxidation state remover failed"
1✔
909

910
    def test_add_oxidation_states_by_guess(self):
1✔
911
        s = PymatgenTest.get_structure("Li2O")
1✔
912
        s.add_oxidation_state_by_guess()
1✔
913
        for i in s:
1✔
914
            assert i.specie in [Species("Li", 1), Species("O", -2)]
1✔
915

916
    def test_add_remove_spin_states(self):
1✔
917
        latt = Lattice.cubic(4.17)
1✔
918
        species = ["Ni", "O"]
1✔
919
        coords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
920
        nio = Structure.from_spacegroup(225, latt, species, coords)
1✔
921

922
        # should do nothing, but not fail
923
        nio.remove_spin()
1✔
924

925
        spins = {"Ni": 5}
1✔
926
        nio.add_spin_by_element(spins)
1✔
927
        assert nio[0].specie.spin == 5, "Failed to add spin states"
1✔
928

929
        nio.remove_spin()
1✔
930
        with pytest.raises(AttributeError):
1✔
931
            nio[0].specie.spin
1✔
932

933
        spins = [5, -5, -5, 5, 0, 0, 0, 0]  # AFM on (001)
1✔
934
        nio.add_spin_by_site(spins)
1✔
935
        assert nio[1].specie.spin == -5, "Failed to add spin states"
1✔
936

937
    def test_apply_operation(self):
1✔
938
        op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 90)
1✔
939
        s = self.structure.copy()
1✔
940
        s.apply_operation(op)
1✔
941
        self.assertArrayAlmostEqual(
1✔
942
            s.lattice.matrix,
943
            [
944
                [0.000000, 3.840198, 0.000000],
945
                [-3.325710, 1.920099, 0.000000],
946
                [2.217138, -0.000000, 3.135509],
947
            ],
948
            5,
949
        )
950

951
        op = SymmOp([[1, 1, 0, 0.5], [1, 0, 0, 0.5], [0, 0, 1, 0.5], [0, 0, 0, 1]])
1✔
952
        s = self.structure.copy()
1✔
953
        s.apply_operation(op, fractional=True)
1✔
954
        self.assertArrayAlmostEqual(
1✔
955
            s.lattice.matrix,
956
            [
957
                [5.760297, 3.325710, 0.000000],
958
                [3.840198, 0.000000, 0.000000],
959
                [0.000000, -2.217138, 3.135509],
960
            ],
961
            5,
962
        )
963

964
    def test_apply_strain(self):
1✔
965
        s = self.structure
1✔
966
        initial_coord = s[1].coords
1✔
967
        s.apply_strain(0.01)
1✔
968
        assert pytest.approx(s.lattice.abc) == (3.8785999130369997, 3.878600984287687, 3.8785999130549516)
1✔
969
        self.assertArrayAlmostEqual(s[1].coords, initial_coord * 1.01)
1✔
970
        a1, b1, c1 = s.lattice.abc
1✔
971
        s.apply_strain([0.1, 0.2, 0.3])
1✔
972
        a2, b2, c2 = s.lattice.abc
1✔
973
        assert round(abs(a2 / a1 - 1.1), 7) == 0
1✔
974
        assert round(abs(b2 / b1 - 1.2), 7) == 0
1✔
975
        assert round(abs(c2 / c1 - 1.3), 7) == 0
1✔
976

977
    def test_scale_lattice(self):
1✔
978
        initial_coord = self.structure[1].coords
1✔
979
        self.structure.scale_lattice(self.structure.volume * 1.01**3)
1✔
980
        self.assertArrayAlmostEqual(
1✔
981
            self.structure.lattice.abc,
982
            (3.8785999130369997, 3.878600984287687, 3.8785999130549516),
983
        )
984
        self.assertArrayAlmostEqual(self.structure[1].coords, initial_coord * 1.01)
1✔
985

986
    def test_translate_sites(self):
1✔
987
        self.structure.translate_sites([0, 1], [0.5, 0.5, 0.5], frac_coords=True)
1✔
988
        self.assertArrayAlmostEqual(self.structure.frac_coords[0], [0.5, 0.5, 0.5])
1✔
989

990
        self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=False)
1✔
991
        self.assertArrayAlmostEqual(self.structure.cart_coords[0], [3.38014845, 1.05428585, 2.06775453])
1✔
992

993
        self.structure.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=True, to_unit_cell=False)
1✔
994
        self.assertArrayAlmostEqual(self.structure.frac_coords[0], [1.00187517, 1.25665291, 1.15946374])
1✔
995

996
        lattice_pbc = Lattice(self.structure.lattice.matrix, pbc=(True, True, False))
1✔
997
        struct_pbc = Structure(lattice_pbc, ["Si"], [[0.75, 0.75, 0.75]])
1✔
998
        struct_pbc.translate_sites([0], [0.5, 0.5, 0.5], frac_coords=True, to_unit_cell=True)
1✔
999
        self.assertArrayAlmostEqual(struct_pbc.frac_coords[0], [0.25, 0.25, 1.25])
1✔
1000

1001
    def test_rotate_sites(self):
1✔
1002
        self.structure.rotate_sites(
1✔
1003
            indices=[1],
1004
            theta=2.0 * np.pi / 3.0,
1005
            anchor=self.structure.sites[0].coords,
1006
            to_unit_cell=False,
1007
        )
1008
        self.assertArrayAlmostEqual(self.structure.frac_coords[1], [-1.25, 1.5, 0.75], decimal=6)
1✔
1009
        self.structure.rotate_sites(
1✔
1010
            indices=[1],
1011
            theta=2.0 * np.pi / 3.0,
1012
            anchor=self.structure.sites[0].coords,
1013
            to_unit_cell=True,
1014
        )
1015
        self.assertArrayAlmostEqual(self.structure.frac_coords[1], [0.75, 0.5, 0.75], decimal=6)
1✔
1016

1017
    def test_mul(self):
1✔
1018
        self.structure *= [2, 1, 1]
1✔
1019
        assert self.structure.formula == "Si4"
1✔
1020
        s = [2, 1, 1] * self.structure
1✔
1021
        assert s.formula == "Si8"
1✔
1022
        assert isinstance(s, Structure)
1✔
1023
        s = self.structure * [[1, 0, 0], [2, 1, 0], [0, 0, 2]]
1✔
1024
        assert s.formula == "Si8"
1✔
1025
        self.assertArrayAlmostEqual(s.lattice.abc, [7.6803959, 17.5979979, 7.6803959])
1✔
1026

1027
    def test_make_supercell(self):
1✔
1028
        self.structure.make_supercell([2, 1, 1])
1✔
1029
        assert self.structure.formula == "Si4"
1✔
1030
        self.structure.make_supercell([[1, 0, 0], [2, 1, 0], [0, 0, 1]])
1✔
1031
        assert self.structure.formula == "Si4"
1✔
1032
        self.structure.make_supercell(2)
1✔
1033
        assert self.structure.formula == "Si32"
1✔
1034
        self.assertArrayAlmostEqual(self.structure.lattice.abc, [15.360792, 35.195996, 7.680396], 5)
1✔
1035

1036
    def test_disordered_supercell_primitive_cell(self):
1✔
1037
        l = Lattice.cubic(2)
1✔
1038
        f = [[0.5, 0.5, 0.5]]
1✔
1039
        sp = [{"Si": 0.54738}]
1✔
1040
        s = Structure(l, sp, f)
1✔
1041
        # this supercell often breaks things
1042
        s.make_supercell([[0, -1, 1], [-1, 1, 0], [1, 1, 1]])
1✔
1043
        assert len(s.get_primitive_structure()) == 1
1✔
1044

1045
    def test_another_supercell(self):
1✔
1046
        # this is included b/c for some reason the old algo was failing on it
1047
        s = self.structure.copy()
1✔
1048
        s.make_supercell([[0, 2, 2], [2, 0, 2], [2, 2, 0]])
1✔
1049
        assert s.formula == "Si32"
1✔
1050
        s = self.structure.copy()
1✔
1051
        s.make_supercell([[0, 2, 0], [1, 0, 0], [0, 0, 1]])
1✔
1052
        assert s.formula == "Si4"
1✔
1053

1054
    def test_to_from_dict(self):
1✔
1055
        d = self.structure.as_dict()
1✔
1056
        s2 = Structure.from_dict(d)
1✔
1057
        assert isinstance(s2, Structure)
1✔
1058

1059
    def test_default_dict_attrs(self):
1✔
1060
        d = self.structure.as_dict()
1✔
1061
        assert d["charge"] == 0
1✔
1062

1063
    def test_to_from_abivars(self):
1✔
1064
        """Test as_dict, from_dict with fmt == abivars."""
1065
        d = self.structure.as_dict(fmt="abivars")
1✔
1066
        s2 = Structure.from_dict(d, fmt="abivars")
1✔
1067
        assert s2 == self.structure
1✔
1068
        assert isinstance(s2, Structure)
1✔
1069

1070
    def test_to_from_file_string(self):
1✔
1071
        with ScratchDir("."):
1✔
1072
            for fmt in ["cif", "json", "poscar", "cssr", "yaml", "xsf", "res"]:
1✔
1073
                s = self.structure.to(fmt=fmt)
1✔
1074
                assert s is not None
1✔
1075
                ss = Structure.from_str(s, fmt=fmt)
1✔
1076
                self.assertArrayAlmostEqual(ss.lattice.parameters, self.structure.lattice.parameters, decimal=5)
1✔
1077
                self.assertArrayAlmostEqual(ss.frac_coords, self.structure.frac_coords)
1✔
1078
                assert isinstance(ss, Structure)
1✔
1079

1080
            self.structure.to(filename="POSCAR.testing")
1✔
1081
            assert os.path.exists("POSCAR.testing")
1✔
1082

1083
            self.structure.to(filename="structure_testing.json")
1✔
1084
            assert Structure.from_file("structure_testing.json") == self.structure
1✔
1085

1086
    def test_from_spacegroup(self):
1✔
1087
        s1 = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]])
1✔
1088
        assert s1.formula == "Li8 O4"
1✔
1089
        s2 = Structure.from_spacegroup(225, Lattice.cubic(3), ["Li", "O"], [[0.25, 0.25, 0.25], [0, 0, 0]])
1✔
1090
        assert s1 == s2
1✔
1091

1092
        s2 = Structure.from_spacegroup(
1✔
1093
            225,
1094
            Lattice.cubic(3),
1095
            ["Li", "O"],
1096
            [[0.25, 0.25, 0.25], [0, 0, 0]],
1097
            site_properties={"charge": [1, -2]},
1098
        )
1099
        assert sum(s2.site_properties["charge"]) == 0
1✔
1100

1101
        s = Structure.from_spacegroup("Pm-3m", Lattice.cubic(3), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
1102
        assert s.formula == "Cs1 Cl1"
1✔
1103

1104
        with pytest.raises(ValueError):
1✔
1105
            Structure.from_spacegroup(
1✔
1106
                "Pm-3m",
1107
                Lattice.tetragonal(1, 3),
1108
                ["Cs", "Cl"],
1109
                [[0, 0, 0], [0.5, 0.5, 0.5]],
1110
            )
1111

1112
        with pytest.raises(ValueError):
1✔
1113
            Structure.from_spacegroup(
1✔
1114
                "Pm-3m",
1115
                Lattice.cubic(3),
1116
                ["Cs"],
1117
                [[0, 0, 0], [0.5, 0.5, 0.5]],
1118
            )
1119
        from fractions import Fraction
1✔
1120

1121
        s = Structure.from_spacegroup(139, np.eye(3), ["H"], [[Fraction(1, 2), Fraction(1, 4), Fraction(0)]])
1✔
1122
        assert len(s) == 8
1✔
1123

1124
    def test_from_magnetic_spacegroup(self):
1✔
1125
        # AFM MnF
1126
        s1 = Structure.from_magnetic_spacegroup(
1✔
1127
            "P4_2'/mnm'",
1128
            Lattice.tetragonal(4.87, 3.30),
1129
            ["Mn", "F"],
1130
            [[0, 0, 0], [0.30, 0.30, 0.00]],
1131
            {"magmom": [4, 0]},
1132
        )
1133

1134
        assert s1.formula == "Mn2 F4"
1✔
1135
        assert sum(map(float, s1.site_properties["magmom"])) == 0
1✔
1136
        assert max(map(float, s1.site_properties["magmom"])) == 4
1✔
1137
        assert min(map(float, s1.site_properties["magmom"])) == -4
1✔
1138

1139
        # AFM LaMnO3, ordered on (001) planes
1140
        s2 = Structure.from_magnetic_spacegroup(
1✔
1141
            "Pn'ma'",
1142
            Lattice.orthorhombic(5.75, 7.66, 5.53),
1143
            ["La", "Mn", "O", "O"],
1144
            [
1145
                [0.05, 0.25, 0.99],
1146
                [0.00, 0.00, 0.50],
1147
                [0.48, 0.25, 0.08],
1148
                [0.31, 0.04, 0.72],
1149
            ],
1150
            {"magmom": [0, Magmom([4, 0, 0]), 0, 0]},
1151
        )
1152

1153
        assert s2.formula == "La4 Mn4 O12"
1✔
1154
        assert sum(map(float, s2.site_properties["magmom"])) == 0
1✔
1155
        assert max(map(float, s2.site_properties["magmom"])) == 4
1✔
1156
        assert min(map(float, s2.site_properties["magmom"])) == -4
1✔
1157

1158
    def test_merge_sites(self):
1✔
1159
        species = [
1✔
1160
            {"Ag": 0.5},
1161
            {"Cl": 0.25},
1162
            {"Cl": 0.1},
1163
            {"Ag": 0.5},
1164
            {"F": 0.15},
1165
            {"F": 0.1},
1166
        ]
1167
        coords = [
1✔
1168
            [0, 0, 0],
1169
            [0.5, 0.5, 0.5],
1170
            [0.5, 0.5, 0.5],
1171
            [0, 0, 0],
1172
            [0.5, 0.5, 1.501],
1173
            [0.5, 0.5, 1.501],
1174
        ]
1175
        s = Structure(Lattice.cubic(1), species, coords)
1✔
1176
        s.merge_sites(mode="s")
1✔
1177
        assert s[0].specie.symbol == "Ag"
1✔
1178
        assert s[1].species == Composition({"Cl": 0.35, "F": 0.25})
1✔
1179
        self.assertArrayAlmostEqual(s[1].frac_coords, [0.5, 0.5, 0.5005])
1✔
1180

1181
        # Test for TaS2 with spacegroup 166 in 160 setting.
1182
        l = Lattice.hexagonal(3.374351, 20.308941)
1✔
1183
        species = ["Ta", "S", "S"]
1✔
1184
        coords = [
1✔
1185
            [0.000000, 0.000000, 0.944333],
1186
            [0.333333, 0.666667, 0.353424],
1187
            [0.666667, 0.333333, 0.535243],
1188
        ]
1189
        tas2 = Structure.from_spacegroup(160, l, species, coords)
1✔
1190
        assert len(tas2) == 13
1✔
1191
        tas2.merge_sites(mode="d")
1✔
1192
        assert len(tas2) == 9
1✔
1193

1194
        l = Lattice.hexagonal(3.587776, 19.622793)
1✔
1195
        species = ["Na", "V", "S", "S"]
1✔
1196
        coords = [
1✔
1197
            [0.333333, 0.666667, 0.165000],
1198
            [0.000000, 0.000000, 0.998333],
1199
            [0.333333, 0.666667, 0.399394],
1200
            [0.666667, 0.333333, 0.597273],
1201
        ]
1202
        navs2 = Structure.from_spacegroup(160, l, species, coords)
1✔
1203
        assert len(navs2) == 18
1✔
1204
        navs2.merge_sites(mode="d")
1✔
1205
        assert len(navs2) == 12
1✔
1206

1207
        # Test that we can average the site properties that are floats
1208
        l = Lattice.hexagonal(3.587776, 19.622793)
1✔
1209
        species = ["Na", "V", "S", "S"]
1✔
1210
        coords = [
1✔
1211
            [0.333333, 0.666667, 0.165000],
1212
            [0.000000, 0.000000, 0.998333],
1213
            [0.333333, 0.666667, 0.399394],
1214
            [0.666667, 0.333333, 0.597273],
1215
        ]
1216
        site_props = {"prop1": [3.0, 5.0, 7.0, 11.0]}
1✔
1217
        navs2 = Structure.from_spacegroup(160, l, species, coords, site_properties=site_props)
1✔
1218
        navs2.insert(0, "Na", coords[0], properties={"prop1": 100.0})
1✔
1219
        navs2.merge_sites(mode="a")
1✔
1220
        assert len(navs2) == 12
1✔
1221
        assert 51.5 in [itr.properties["prop1"] for itr in navs2.sites]
1✔
1222

1223
    def test_properties(self):
1✔
1224
        assert self.structure.num_sites == len(self.structure)
1✔
1225
        self.structure.make_supercell(2)
1✔
1226
        self.structure[1] = "C"
1✔
1227
        sites = list(self.structure.group_by_types())
1✔
1228
        assert sites[-1].specie.symbol == "C"
1✔
1229
        self.structure.add_oxidation_state_by_element({"Si": 4, "C": 2})
1✔
1230
        assert self.structure.charge == 62
1✔
1231

1232
    def test_set_item(self):
1✔
1233
        s = self.structure.copy()
1✔
1234
        s[0] = "C"
1✔
1235
        assert s.formula == "Si1 C1"
1✔
1236
        s[(0, 1)] = "Ge"
1✔
1237
        assert s.formula == "Ge2"
1✔
1238
        s[0:2] = "Sn"
1✔
1239
        assert s.formula == "Sn2"
1✔
1240

1241
        s = self.structure.copy()
1✔
1242
        s["Si"] = "C"
1✔
1243
        assert s.formula == "C2"
1✔
1244
        s["C"] = "C0.25Si0.5"
1✔
1245
        assert s.formula == "Si1 C0.5"
1✔
1246
        s["C"] = "C0.25Si0.5"
1✔
1247
        assert s.formula == "Si1.25 C0.125"
1✔
1248

1249
    def test_init_error(self):
1✔
1250
        with pytest.raises(StructureError):
1✔
1251
            Structure(
1✔
1252
                Lattice.cubic(3),
1253
                ["Si"],
1254
                [[0, 0, 0], [0.5, 0.5, 0.5]],
1255
            )
1256

1257
    def test_from_sites(self):
1✔
1258
        self.structure.add_site_property("hello", [1, 2])
1✔
1259
        s = Structure.from_sites(self.structure, to_unit_cell=True)
1✔
1260
        assert s.site_properties["hello"][1] == 2
1✔
1261

1262
    def test_charge(self):
1✔
1263
        s = Structure.from_sites(self.structure)
1✔
1264
        assert s.charge == 0, "Initial Structure not defaulting to behavior in SiteCollection"
1✔
1265
        s.add_oxidation_state_by_site([1, 1])
1✔
1266
        assert s.charge == 2, "Initial Structure not defaulting to behavior in SiteCollection"
1✔
1267
        s = Structure.from_sites(s, charge=1)
1✔
1268
        assert s.charge == 1, "Overall charge not being stored in separate property"
1✔
1269
        s = s.copy()
1✔
1270
        assert s.charge == 1, "Overall charge not being copied properly with no sanitization"
1✔
1271
        s = s.copy(sanitize=True)
1✔
1272
        assert s.charge == 1, "Overall charge not being copied properly with sanitization"
1✔
1273
        super_cell = s * 3
1✔
1274
        assert super_cell.charge == 27, "Overall charge is not being properly multiplied in IStructure __mul__"
1✔
1275
        assert "Overall Charge: +1" in str(s), "String representation not adding charge"
1✔
1276
        sorted_s = super_cell.get_sorted_structure()
1✔
1277
        assert sorted_s.charge == 27, "Overall charge is not properly copied during structure sorting"
1✔
1278
        super_cell.set_charge(25)
1✔
1279
        assert super_cell.charge == 25, "Set charge not properly modifying _charge"
1✔
1280

1281
    def test_vesta_lattice_matrix(self):
1✔
1282
        silica_zeolite = Molecule.from_file(self.TEST_FILES_DIR / "CON_vesta.xyz")
1✔
1283

1284
        s_vesta = Structure(
1✔
1285
            lattice=Lattice.from_parameters(22.6840, 13.3730, 12.5530, 90, 69.479, 90, True),
1286
            species=silica_zeolite.species,
1287
            coords=silica_zeolite.cart_coords,
1288
            coords_are_cartesian=True,
1289
            to_unit_cell=True,
1290
        )
1291

1292
        s_vesta = s_vesta.get_primitive_structure()
1✔
1293
        s_vesta.merge_sites(0.01, "delete")
1✔
1294
        assert s_vesta.formula == "Si56 O112"
1✔
1295

1296
        broken_s = Structure(
1✔
1297
            lattice=Lattice.from_parameters(22.6840, 13.3730, 12.5530, 90, 69.479, 90),
1298
            species=silica_zeolite.species,
1299
            coords=silica_zeolite.cart_coords,
1300
            coords_are_cartesian=True,
1301
            to_unit_cell=True,
1302
        )
1303

1304
        broken_s.merge_sites(0.01, "delete")
1✔
1305
        assert broken_s.formula == "Si56 O134"
1✔
1306

1307
    def test_extract_cluster(self):
1✔
1308
        coords = [
1✔
1309
            [0.000000, 0.000000, 0.000000],
1310
            [0.000000, 0.000000, 1.089000],
1311
            [1.026719, 0.000000, -0.363000],
1312
            [-0.513360, -0.889165, -0.363000],
1313
            [-0.513360, 0.889165, -0.363000],
1314
        ]
1315
        ch4 = ["C", "H", "H", "H", "H"]
1✔
1316

1317
        species = []
1✔
1318
        allcoords = []
1✔
1319
        for vec in ([0, 0, 0], [4, 0, 0], [0, 4, 0], [4, 4, 0]):
1✔
1320
            species.extend(ch4)
1✔
1321
            for c in coords:
1✔
1322
                allcoords.append(np.array(c) + vec)
1✔
1323

1324
        structure = Structure(Lattice.cubic(10), species, allcoords, coords_are_cartesian=True)
1✔
1325

1326
        for site in structure:
1✔
1327
            if site.specie.symbol == "C":
1✔
1328
                cluster = Molecule.from_sites(structure.extract_cluster([site]))
1✔
1329
                assert cluster.formula == "H4 C1"
1✔
1330

1331
    @unittest.skipIf(m3gnet is None, "Relaxation test requires m3gnet.")
1✔
1332
    def test_relax(self):
1✔
1333
        structure = self.get_structure("Si")
1✔
1334
        relaxed = structure.relax()
1✔
1335
        assert round(abs(relaxed.lattice.a - 3.849563), 4) == 0
1✔
1336

1337
    def test_from_prototype(self):
1✔
1338
        for pt in ["bcc", "fcc", "hcp", "diamond"]:
1✔
1339
            s = Structure.from_prototype(pt, ["C"], a=3, c=4)
1✔
1340
            assert isinstance(s, Structure)
1✔
1341

1342
        with pytest.raises(ValueError):
1✔
1343
            Structure.from_prototype("hcp", ["C"], a=3)
1✔
1344

1345
        s = Structure.from_prototype("rocksalt", ["Li", "Cl"], a=2.56)
1✔
1346
        assert (
1✔
1347
            str(s)
1348
            == """Full Formula (Li4 Cl4)
1349
Reduced Formula: LiCl
1350
abc   :   2.560000   2.560000   2.560000
1351
angles:  90.000000  90.000000  90.000000
1352
pbc   :       True       True       True
1353
Sites (8)
1354
  #  SP      a    b    c
1355
---  ----  ---  ---  ---
1356
  0  Li    0    0    0
1357
  1  Li    0.5  0.5  0
1358
  2  Li    0.5  0    0.5
1359
  3  Li    0    0.5  0.5
1360
  4  Cl    0.5  0    0.5
1361
  5  Cl    0    0.5  0.5
1362
  6  Cl    0.5  0.5  0
1363
  7  Cl    0    0    0"""
1364
        )
1365
        for pt in ("cscl", "fluorite", "antifluorite", "zincblende"):
1✔
1366
            s = Structure.from_prototype(pt, ["Cs", "Cl"], a=5)
1✔
1367
            assert s.lattice.is_orthogonal
1✔
1368

1369

1370
class IMoleculeTest(PymatgenTest):
1✔
1371
    def setUp(self):
1✔
1372
        coords = [
1✔
1373
            [0.000000, 0.000000, 0.000000],
1374
            [0.000000, 0.000000, 1.089000],
1375
            [1.026719, 0.000000, -0.363000],
1376
            [-0.513360, -0.889165, -0.363000],
1377
            [-0.513360, 0.889165, -0.363000],
1378
        ]
1379
        self.coords = coords
1✔
1380
        self.mol = Molecule(["C", "H", "H", "H", "H"], coords)
1✔
1381

1382
    def test_set_item(self):
1✔
1383
        s = self.mol.copy()
1✔
1384
        s[0] = "Si"
1✔
1385
        assert s.formula == "Si1 H4"
1✔
1386
        s[(0, 1)] = "Ge"
1✔
1387
        assert s.formula == "Ge2 H3"
1✔
1388
        s[0:2] = "Sn"
1✔
1389
        assert s.formula == "Sn2 H3"
1✔
1390

1391
        s = self.mol.copy()
1✔
1392
        s["H"] = "F"
1✔
1393
        assert s.formula == "C1 F4"
1✔
1394
        s["C"] = "C0.25Si0.5"
1✔
1395
        assert s.formula == "Si0.5 C0.25 F4"
1✔
1396
        s["C"] = "C0.25Si0.5"
1✔
1397
        assert s.formula == "Si0.625 C0.0625 F4"
1✔
1398

1399
    def test_bad_molecule(self):
1✔
1400
        coords = [
1✔
1401
            [0.000000, 0.000000, 0.000000],
1402
            [0.000000, 0.000000, 1.089000],
1403
            [1.026719, 0.000000, -0.363000],
1404
            [-0.513360, -0.889165, -0.363000],
1405
            [-0.513360, 0.889165, -0.363000],
1406
            [-0.513360, 0.889165, -0.36301],
1407
        ]
1408
        with pytest.raises(StructureError):
1✔
1409
            Molecule(
1✔
1410
                ["C", "H", "H", "H", "H", "H"],
1411
                coords,
1412
                validate_proximity=True,
1413
            )
1414

1415
    def test_get_angle_dihedral(self):
1✔
1416
        assert round(abs(self.mol.get_angle(1, 0, 2) - 109.47122144618737), 7) == 0
1✔
1417
        assert round(abs(self.mol.get_angle(3, 1, 2) - 60.00001388659683), 7) == 0
1✔
1418
        assert round(abs(self.mol.get_dihedral(0, 1, 2, 3) - -35.26438851071765), 7) == 0
1✔
1419

1420
        coords = []
1✔
1421
        coords.append([0, 0, 0])
1✔
1422
        coords.append([0, 0, 1])
1✔
1423
        coords.append([0, 1, 1])
1✔
1424
        coords.append([1, 1, 1])
1✔
1425
        self.mol2 = Molecule(["C", "O", "N", "S"], coords)
1✔
1426
        assert round(abs(self.mol2.get_dihedral(0, 1, 2, 3) - -90), 7) == 0
1✔
1427

1428
    def test_get_covalent_bonds(self):
1✔
1429
        assert len(self.mol.get_covalent_bonds()) == 4
1✔
1430

1431
    def test_properties(self):
1✔
1432
        assert len(self.mol) == 5
1✔
1433
        assert self.mol.is_ordered
1✔
1434
        assert self.mol.formula == "H4 C1"
1✔
1435

1436
    def test_repr_str(self):
1✔
1437
        ans = """Full Formula (H4 C1)
1✔
1438
Reduced Formula: H4C
1439
Charge = 0.0, Spin Mult = 1
1440
Sites (5)
1441
0 C     0.000000     0.000000     0.000000
1442
1 H     0.000000     0.000000     1.089000
1443
2 H     1.026719     0.000000    -0.363000
1444
3 H    -0.513360    -0.889165    -0.363000
1445
4 H    -0.513360     0.889165    -0.363000"""
1446
        assert str(self.mol) == ans
1✔
1447
        ans = """Molecule Summary
1✔
1448
Site: C (0.0000, 0.0000, 0.0000)
1449
Site: H (0.0000, 0.0000, 1.0890)
1450
Site: H (1.0267, 0.0000, -0.3630)
1451
Site: H (-0.5134, -0.8892, -0.3630)
1452
Site: H (-0.5134, 0.8892, -0.3630)"""
1453
        assert repr(self.mol) == ans
1✔
1454

1455
    def test_site_properties(self):
1✔
1456
        propertied_mol = Molecule(
1✔
1457
            ["C", "H", "H", "H", "H"],
1458
            self.coords,
1459
            site_properties={"magmom": [0.5, -0.5, 1, 2, 3]},
1460
        )
1461
        assert propertied_mol[0].magmom == 0.5
1✔
1462
        assert propertied_mol[1].magmom == -0.5
1✔
1463

1464
    def test_get_boxed_structure(self):
1✔
1465
        s = self.mol.get_boxed_structure(9, 9, 9)
1✔
1466
        # C atom should be in center of box.
1467
        self.assertArrayAlmostEqual(s[4].frac_coords, [0.50000001, 0.5, 0.5])
1✔
1468
        self.assertArrayAlmostEqual(s[1].frac_coords, [0.6140799, 0.5, 0.45966667])
1✔
1469
        with pytest.raises(ValueError):
1✔
1470
            self.mol.get_boxed_structure(1, 1, 1)
1✔
1471
        s2 = self.mol.get_boxed_structure(5, 5, 5, (2, 3, 4))
1✔
1472
        assert len(s2) == 24 * 5
1✔
1473
        assert s2.lattice.abc == (10, 15, 20)
1✔
1474

1475
        # Test offset option
1476
        s3 = self.mol.get_boxed_structure(9, 9, 9, offset=[0.5, 0.5, 0.5])
1✔
1477
        self.assertArrayAlmostEqual(s3[4].coords, [5, 5, 5])
1✔
1478
        # Test no_cross option
1479
        with pytest.raises(ValueError):
1✔
1480
            self.mol.get_boxed_structure(
1✔
1481
                5,
1482
                5,
1483
                5,
1484
                offset=[10, 10, 10],
1485
                no_cross=True,
1486
            )
1487

1488
        # Test reorder option
1489
        no_reorder = self.mol.get_boxed_structure(10, 10, 10, reorder=False)
1✔
1490
        assert str(s3[0].specie) == "H"
1✔
1491
        assert str(no_reorder[0].specie) == "C"
1✔
1492

1493
    def test_get_distance(self):
1✔
1494
        assert round(abs(self.mol.get_distance(0, 1) - 1.089), 7) == 0
1✔
1495

1496
    def test_get_neighbors(self):
1✔
1497
        nn = self.mol.get_neighbors(self.mol[0], 1)
1✔
1498
        assert len(nn) == 0
1✔
1499
        nn = self.mol.get_neighbors(self.mol[0], 2)
1✔
1500
        assert len(nn) == 4
1✔
1501

1502
    def test_get_neighbors_in_shell(self):
1✔
1503
        nn = self.mol.get_neighbors_in_shell([0, 0, 0], 0, 1)
1✔
1504
        assert len(nn) == 1
1✔
1505
        nn = self.mol.get_neighbors_in_shell([0, 0, 0], 1, 0.9)
1✔
1506
        assert len(nn) == 4
1✔
1507
        nn = self.mol.get_neighbors_in_shell([0, 0, 0], 1, 0.9)
1✔
1508
        assert len(nn) == 4
1✔
1509
        nn = self.mol.get_neighbors_in_shell([0, 0, 0], 2, 0.1)
1✔
1510
        assert len(nn) == 0
1✔
1511

1512
    def test_get_dist_matrix(self):
1✔
1513
        ans = [
1✔
1514
            [0.0, 1.089, 1.08899995636, 1.08900040717, 1.08900040717],
1515
            [1.089, 0.0, 1.77832952654, 1.7783298026, 1.7783298026],
1516
            [1.08899995636, 1.77832952654, 0.0, 1.77833003783, 1.77833003783],
1517
            [1.08900040717, 1.7783298026, 1.77833003783, 0.0, 1.77833],
1518
            [1.08900040717, 1.7783298026, 1.77833003783, 1.77833, 0.0],
1519
        ]
1520
        self.assertArrayAlmostEqual(self.mol.distance_matrix, ans)
1✔
1521

1522
    def test_break_bond(self):
1✔
1523
        (mol1, mol2) = self.mol.break_bond(0, 1)
1✔
1524
        assert mol1.formula == "H3 C1"
1✔
1525
        assert mol2.formula == "H1"
1✔
1526

1527
    def test_prop(self):
1✔
1528
        assert self.mol.charge == 0
1✔
1529
        assert self.mol.spin_multiplicity == 1
1✔
1530
        assert self.mol.nelectrons == 10
1✔
1531
        self.assertArrayAlmostEqual(self.mol.center_of_mass, [0, 0, 0])
1✔
1532
        with pytest.raises(ValueError):
1✔
1533
            Molecule(
1✔
1534
                ["C", "H", "H", "H", "H"],
1535
                self.coords,
1536
                charge=1,
1537
                spin_multiplicity=1,
1538
            )
1539
        mol = Molecule(["C", "H", "H", "H", "H"], self.coords, charge=1)
1✔
1540
        assert mol.spin_multiplicity == 2
1✔
1541
        assert mol.nelectrons == 9
1✔
1542

1543
        # Triplet O2
1544
        mol = IMolecule(["O"] * 2, [[0, 0, 0], [0, 0, 1.2]], spin_multiplicity=3)
1✔
1545
        assert mol.spin_multiplicity == 3
1✔
1546

1547
    def test_no_spin_check(self):
1✔
1548
        coords = [
1✔
1549
            [0.000000, 0.000000, 0.000000],
1550
            [0.000000, 0.000000, 1.089000],
1551
            [1.026719, 0.000000, -0.363000],
1552
            [-0.513360, -0.889165, -0.363000],
1553
        ]
1554
        with pytest.raises(ValueError):
1✔
1555
            mol = IMolecule(["C", "H", "H", "H"], coords, charge=0, spin_multiplicity=1)
1✔
1556
        mol = IMolecule(["C", "H", "H", "H"], coords, charge=0, spin_multiplicity=1, charge_spin_check=False)
1✔
1557
        assert mol.spin_multiplicity == 1
1✔
1558
        assert mol.charge == 0
1✔
1559

1560
    def test_equal(self):
1✔
1561
        mol = IMolecule(["C", "H", "H", "H", "H"], self.coords, charge=1)
1✔
1562
        assert mol != self.mol
1✔
1563

1564
    def test_get_centered_molecule(self):
1✔
1565
        mol = IMolecule(["O"] * 2, [[0, 0, 0], [0, 0, 1.2]], spin_multiplicity=3)
1✔
1566
        centered = mol.get_centered_molecule()
1✔
1567
        self.assertArrayAlmostEqual(centered.center_of_mass, [0, 0, 0])
1✔
1568

1569
    def test_to_from_dict(self):
1✔
1570
        d = self.mol.as_dict()
1✔
1571
        mol2 = IMolecule.from_dict(d)
1✔
1572
        assert isinstance(mol2, IMolecule)
1✔
1573
        propertied_mol = Molecule(
1✔
1574
            ["C", "H", "H", "H", "H"],
1575
            self.coords,
1576
            charge=1,
1577
            site_properties={"magmom": [0.5, -0.5, 1, 2, 3]},
1578
        )
1579
        d = propertied_mol.as_dict()
1✔
1580
        assert d["sites"][0]["properties"]["magmom"] == 0.5
1✔
1581
        mol = Molecule.from_dict(d)
1✔
1582
        assert propertied_mol == mol
1✔
1583
        assert mol[0].magmom == 0.5
1✔
1584
        assert mol.formula == "H4 C1"
1✔
1585
        assert mol.charge == 1
1✔
1586

1587
    def test_default_dict_attrs(self):
1✔
1588
        d = self.mol.as_dict()
1✔
1589
        assert d["charge"] == 0
1✔
1590
        assert d["spin_multiplicity"] == 1
1✔
1591

1592
    def test_to_from_file_string(self):
1✔
1593
        for fmt in ["xyz", "json", "g03", "yaml"]:
1✔
1594
            s = self.mol.to(fmt=fmt)
1✔
1595
            assert s is not None
1✔
1596
            m = IMolecule.from_str(s, fmt=fmt)
1✔
1597
            assert m == self.mol
1✔
1598
            assert isinstance(m, IMolecule)
1✔
1599

1600
        self.mol.to(filename="CH4_testing.xyz")
1✔
1601
        assert os.path.exists("CH4_testing.xyz")
1✔
1602
        os.remove("CH4_testing.xyz")
1✔
1603
        self.mol.to(filename="CH4_testing.yaml")
1✔
1604
        assert os.path.exists("CH4_testing.yaml")
1✔
1605
        mol = Molecule.from_file("CH4_testing.yaml")
1✔
1606
        assert self.mol == mol
1✔
1607
        os.remove("CH4_testing.yaml")
1✔
1608

1609

1610
class MoleculeTest(PymatgenTest):
1✔
1611
    def setUp(self):
1✔
1612
        coords = [
1✔
1613
            [0.000000, 0.000000, 0.000000],
1614
            [0.000000, 0.000000, 1.089000],
1615
            [1.026719, 0.000000, -0.363000],
1616
            [-0.513360, -0.889165, -0.363000],
1617
            [-0.513360, 0.889165, -0.363000],
1618
        ]
1619
        self.mol = Molecule(["C", "H", "H", "H", "H"], coords)
1✔
1620
        warnings.simplefilter("ignore")
1✔
1621

1622
    def tearDown(self):
1✔
1623
        warnings.simplefilter("default")
1✔
1624

1625
    def test_mutable_sequence_methods(self):
1✔
1626
        s = self.mol
1✔
1627
        s[1] = ("F", [0.5, 0.5, 0.5])
1✔
1628
        assert s.formula == "H3 C1 F1"
1✔
1629
        self.assertArrayAlmostEqual(s[1].coords, [0.5, 0.5, 0.5])
1✔
1630
        s.reverse()
1✔
1631
        assert s[0].specie == Element("H")
1✔
1632
        self.assertArrayAlmostEqual(s[0].coords, [-0.513360, 0.889165, -0.363000])
1✔
1633
        del s[1]
1✔
1634
        assert s.formula == "H2 C1 F1"
1✔
1635
        s[3] = "N", [0, 0, 0], {"charge": 4}
1✔
1636
        assert s.formula == "H2 N1 F1"
1✔
1637
        assert s[3].charge == 4
1✔
1638

1639
    def test_insert_remove_append(self):
1✔
1640
        mol = self.mol
1✔
1641
        mol.insert(1, "O", [0.5, 0.5, 0.5])
1✔
1642
        assert mol.formula == "H4 C1 O1"
1✔
1643
        del mol[2]
1✔
1644
        assert mol.formula == "H3 C1 O1"
1✔
1645
        mol.set_charge_and_spin(0)
1✔
1646
        assert mol.spin_multiplicity == 2
1✔
1647
        mol.append("N", [1, 1, 1])
1✔
1648
        assert mol.formula == "H3 C1 N1 O1"
1✔
1649
        with pytest.raises(TypeError):
1✔
1650
            dict([(mol, 1)])
1✔
1651
        mol.remove_sites([0, 1])
1✔
1652
        assert mol.formula == "H3 N1"
1✔
1653

1654
    def test_translate_sites(self):
1✔
1655
        self.mol.translate_sites([0, 1], [0.5, 0.5, 0.5])
1✔
1656
        self.assertArrayEqual(self.mol.cart_coords[0], [0.5, 0.5, 0.5])
1✔
1657

1658
    def test_rotate_sites(self):
1✔
1659
        self.mol.rotate_sites(theta=np.radians(30))
1✔
1660
        self.assertArrayAlmostEqual(self.mol.cart_coords[2], [0.889164737, 0.513359500, -0.363000000])
1✔
1661

1662
    def test_replace(self):
1✔
1663
        self.mol[0] = "Ge"
1✔
1664
        assert self.mol.formula == "Ge1 H4"
1✔
1665

1666
        self.mol.replace_species({Element("Ge"): {Element("Ge"): 0.5, Element("Si"): 0.5}})
1✔
1667
        assert self.mol.formula == "Si0.5 Ge0.5 H4"
1✔
1668

1669
        # this should change the .5Si .5Ge sites to .75Si .25Ge
1670
        self.mol.replace_species({Element("Ge"): {Element("Ge"): 0.5, Element("Si"): 0.5}})
1✔
1671
        assert self.mol.formula == "Si0.75 Ge0.25 H4"
1✔
1672

1673
        d = 0.1
1✔
1674
        pre_perturbation_sites = self.mol.sites[:]
1✔
1675
        self.mol.perturb(distance=d)
1✔
1676
        post_perturbation_sites = self.mol.sites
1✔
1677

1678
        for i, x in enumerate(pre_perturbation_sites):
1✔
1679
            assert round(abs(x.distance(post_perturbation_sites[i]) - d), 3) == 0, "Bad perturbation distance"
1✔
1680

1681
    def test_add_site_property(self):
1✔
1682
        self.mol.add_site_property("charge", [4.1, -2, -2, -2, -2])
1✔
1683
        assert self.mol[0].charge == 4.1
1✔
1684
        assert self.mol[1].charge == -2
1✔
1685

1686
        self.mol.add_site_property("magmom", [3, 2, 2, 2, 2])
1✔
1687
        assert self.mol[0].charge == 4.1
1✔
1688
        assert self.mol[0].magmom == 3
1✔
1689
        self.mol.remove_site_property("magmom")
1✔
1690
        with pytest.raises(AttributeError):
1✔
1691
            self.mol[0].magmom
1✔
1692

1693
    def test_to_from_dict(self):
1✔
1694
        self.mol.append("X", [2, 0, 0])
1✔
1695
        d = self.mol.as_dict()
1✔
1696
        mol2 = Molecule.from_dict(d)
1✔
1697
        assert isinstance(mol2, Molecule)
1✔
1698
        self.assertMSONable(self.mol)
1✔
1699

1700
    def test_apply_operation(self):
1✔
1701
        op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 90)
1✔
1702
        self.mol.apply_operation(op)
1✔
1703
        self.assertArrayAlmostEqual(self.mol[2].coords, [0.000000, 1.026719, -0.363000])
1✔
1704

1705
    def test_substitute(self):
1✔
1706
        coords = [
1✔
1707
            [0.000000, 0.000000, 1.08],
1708
            [0.000000, 0.000000, 0.000000],
1709
            [1.026719, 0.000000, -0.363000],
1710
            [-0.513360, -0.889165, -0.363000],
1711
            [-0.513360, 0.889165, -0.363000],
1712
        ]
1713
        sub = Molecule(["X", "C", "H", "H", "H"], coords)
1✔
1714
        self.mol.substitute(1, sub)
1✔
1715
        assert round(abs(self.mol.get_distance(0, 4) - 1.54), 7) == 0
1✔
1716
        f = Molecule(["X", "F"], [[0, 0, 0], [0, 0, 1.11]])
1✔
1717
        self.mol.substitute(2, f)
1✔
1718
        assert round(abs(self.mol.get_distance(0, 7) - 1.35), 7) == 0
1✔
1719
        oh = Molecule(
1✔
1720
            ["X", "O", "H"],
1721
            [[0, 0.780362, -0.456316], [0, 0, 0.114079], [0, -0.780362, -0.456316]],
1722
        )
1723
        self.mol.substitute(1, oh)
1✔
1724
        assert round(abs(self.mol.get_distance(0, 7) - 1.43), 7) == 0
1✔
1725
        self.mol.substitute(3, "methyl")
1✔
1726
        assert self.mol.formula == "H7 C3 O1 F1"
1✔
1727
        coords = [
1✔
1728
            [0.00000, 1.40272, 0.00000],
1729
            [0.00000, 2.49029, 0.00000],
1730
            [-1.21479, 0.70136, 0.00000],
1731
            [-2.15666, 1.24515, 0.00000],
1732
            [-1.21479, -0.70136, 0.00000],
1733
            [-2.15666, -1.24515, 0.00000],
1734
            [0.00000, -1.40272, 0.00000],
1735
            [0.00000, -2.49029, 0.00000],
1736
            [1.21479, -0.70136, 0.00000],
1737
            [2.15666, -1.24515, 0.00000],
1738
            [1.21479, 0.70136, 0.00000],
1739
            [2.15666, 1.24515, 0.00000],
1740
        ]
1741
        benzene = Molecule(["C", "H", "C", "H", "C", "H", "C", "H", "C", "H", "C", "H"], coords)
1✔
1742
        benzene.substitute(1, sub)
1✔
1743
        assert benzene.formula == "H8 C7"
1✔
1744
        # Carbon attached should be in plane.
1745
        assert round(abs(benzene[11].coords[2] - 0), 7) == 0
1✔
1746
        benzene[14] = "Br"
1✔
1747
        benzene.substitute(13, sub)
1✔
1748
        assert benzene.formula == "H9 C8 Br1"
1✔
1749

1750
    def test_to_from_file_string(self):
1✔
1751
        for fmt in ["xyz", "json", "g03"]:
1✔
1752
            s = self.mol.to(fmt=fmt)
1✔
1753
            assert s is not None
1✔
1754
            m = Molecule.from_str(s, fmt=fmt)
1✔
1755
            assert m == self.mol
1✔
1756
            assert isinstance(m, Molecule)
1✔
1757

1758
        self.mol.to(filename="CH4_testing.xyz")
1✔
1759
        assert os.path.exists("CH4_testing.xyz")
1✔
1760
        os.remove("CH4_testing.xyz")
1✔
1761

1762
    def test_extract_cluster(self):
1✔
1763
        species = self.mol.species * 2
1✔
1764
        coords = list(self.mol.cart_coords) + list(self.mol.cart_coords + [10, 0, 0])
1✔
1765
        mol = Molecule(species, coords)
1✔
1766
        cluster = Molecule.from_sites(mol.extract_cluster([mol[0]]))
1✔
1767
        assert mol.formula == "H8 C2"
1✔
1768
        assert cluster.formula == "H4 C1"
1✔
1769

1770
    def test_no_spin_check(self):
1✔
1771
        coords = [
1✔
1772
            [0.000000, 0.000000, 0.000000],
1773
            [0.000000, 0.000000, 1.089000],
1774
            [1.026719, 0.000000, -0.363000],
1775
            [-0.513360, -0.889165, -0.363000],
1776
        ]
1777
        with pytest.raises(ValueError):
1✔
1778
            mol = Molecule(["C", "H", "H", "H"], coords, charge=0, spin_multiplicity=1)
1✔
1779
        mol_valid = Molecule(["C", "H", "H", "H"], coords, charge=0, spin_multiplicity=2)
1✔
1780
        with pytest.raises(ValueError):
1✔
1781
            mol_valid.set_charge_and_spin(0, 1)
1✔
1782
        mol = Molecule(["C", "H", "H", "H"], coords, charge=0, spin_multiplicity=1, charge_spin_check=False)
1✔
1783
        assert mol.spin_multiplicity == 1
1✔
1784
        assert mol.charge == 0
1✔
1785
        mol.set_charge_and_spin(0, 3)
1✔
1786
        assert mol.charge == 0
1✔
1787
        assert mol.spin_multiplicity == 3
1✔
1788

1789

1790
if __name__ == "__main__":
1✔
1791
    unittest.main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc