• 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

95.59
/pymatgen/core/tests/test_surface.py
1
#!/usr/bin/python
2

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

11
import numpy as np
1✔
12

13
from pymatgen.analysis.structure_matcher import StructureMatcher
1✔
14
from pymatgen.core.lattice import Lattice
1✔
15
from pymatgen.core.structure import Structure
1✔
16
from pymatgen.core.surface import (
1✔
17
    ReconstructionGenerator,
18
    Slab,
19
    SlabGenerator,
20
    generate_all_slabs,
21
    get_d,
22
    get_slab_regions,
23
    get_symmetrically_distinct_miller_indices,
24
    get_symmetrically_equivalent_miller_indices,
25
    miller_index_from_sites,
26
)
27
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
28
from pymatgen.symmetry.groups import SpaceGroup
1✔
29
from pymatgen.util.testing import PymatgenTest
1✔
30

31

32
def get_path(path_str):
1✔
33
    path = os.path.join(PymatgenTest.TEST_FILES_DIR, "surface_tests", path_str)
1✔
34
    return path
1✔
35

36

37
class SlabTest(PymatgenTest):
1✔
38
    def setUp(self):
1✔
39
        zno1 = Structure.from_file(get_path("ZnO-wz.cif"), primitive=False)
1✔
40
        zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False).get_slab()
1✔
41

42
        Ti = Structure(
1✔
43
            Lattice.hexagonal(4.6, 2.82),
44
            ["Ti", "Ti", "Ti"],
45
            [
46
                [0.000000, 0.000000, 0.000000],
47
                [0.333333, 0.666667, 0.500000],
48
                [0.666667, 0.333333, 0.500000],
49
            ],
50
        )
51

52
        Ag_fcc = Structure(
1✔
53
            Lattice.cubic(4.06),
54
            ["Ag", "Ag", "Ag", "Ag"],
55
            [
56
                [0.000000, 0.000000, 0.000000],
57
                [0.000000, 0.500000, 0.500000],
58
                [0.500000, 0.000000, 0.500000],
59
                [0.500000, 0.500000, 0.000000],
60
            ],
61
        )
62

63
        m = [[3.913449, 0, 0], [0, 3.913449, 0], [0, 0, 5.842644]]
1✔
64
        latt = Lattice(m)
1✔
65
        fcoords = [[0.5, 0, 0.222518], [0, 0.5, 0.777482], [0, 0, 0], [0, 0, 0.5], [0.5, 0.5, 0]]
1✔
66
        non_laue = Structure(latt, ["Nb", "Nb", "N", "N", "N"], fcoords)
1✔
67

68
        self.ti = Ti
1✔
69
        self.agfcc = Ag_fcc
1✔
70
        self.zno1 = zno1
1✔
71
        self.zno55 = zno55
1✔
72
        self.nonlaue = non_laue
1✔
73
        self.h = Structure(Lattice.cubic(3), ["H"], [[0, 0, 0]])
1✔
74
        self.libcc = Structure(Lattice.cubic(3.51004), ["Li", "Li"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
75

76
    def test_init(self):
1✔
77
        zno_slab = Slab(
1✔
78
            self.zno55.lattice,
79
            self.zno55.species,
80
            self.zno55.frac_coords,
81
            self.zno55.miller_index,
82
            self.zno55.oriented_unit_cell,
83
            0,
84
            self.zno55.scale_factor,
85
        )
86
        m = self.zno55.lattice.matrix
1✔
87
        area = np.linalg.norm(np.cross(m[0], m[1]))
1✔
88
        assert round(abs(zno_slab.surface_area - area), 7) == 0
1✔
89
        assert zno_slab.lattice.parameters == self.zno55.lattice.parameters
1✔
90
        assert zno_slab.oriented_unit_cell.composition == self.zno1.composition
1✔
91
        assert len(zno_slab) == 8
1✔
92

93
        # check reorient_lattice. get a slab not oriented and check that orientation
94
        # works even with Cartesian coordinates.
95
        zno_not_or = SlabGenerator(
1✔
96
            self.zno1,
97
            [1, 0, 0],
98
            5,
99
            5,
100
            lll_reduce=False,
101
            center_slab=False,
102
            reorient_lattice=False,
103
        ).get_slab()
104
        zno_slab_cart = Slab(
1✔
105
            zno_not_or.lattice,
106
            zno_not_or.species,
107
            zno_not_or.cart_coords,
108
            zno_not_or.miller_index,
109
            zno_not_or.oriented_unit_cell,
110
            0,
111
            zno_not_or.scale_factor,
112
            coords_are_cartesian=True,
113
            reorient_lattice=True,
114
        )
115
        self.assertArrayAlmostEqual(zno_slab.frac_coords, zno_slab_cart.frac_coords)
1✔
116
        c = zno_slab_cart.lattice.matrix[2]
1✔
117
        self.assertArrayAlmostEqual([0, 0, np.linalg.norm(c)], c)
1✔
118

119
    def test_add_adsorbate_atom(self):
1✔
120
        zno_slab = Slab(
1✔
121
            self.zno55.lattice,
122
            self.zno55.species,
123
            self.zno55.frac_coords,
124
            self.zno55.miller_index,
125
            self.zno55.oriented_unit_cell,
126
            0,
127
            self.zno55.scale_factor,
128
        )
129
        zno_slab.add_adsorbate_atom([1], "H", 1)
1✔
130

131
        assert len(zno_slab) == 9
1✔
132
        assert str(zno_slab[8].specie) == "H"
1✔
133
        assert round(abs(zno_slab.get_distance(1, 8) - 1.0), 7) == 0
1✔
134
        assert zno_slab[8].c > zno_slab[0].c
1✔
135
        m = self.zno55.lattice.matrix
1✔
136
        area = np.linalg.norm(np.cross(m[0], m[1]))
1✔
137
        assert round(abs(zno_slab.surface_area - area), 7) == 0
1✔
138
        assert zno_slab.lattice.parameters == self.zno55.lattice.parameters
1✔
139

140
    def test_get_sorted_structure(self):
1✔
141
        species = [str(site.specie) for site in self.zno55.get_sorted_structure()]
1✔
142
        assert species == ["Zn2+"] * 4 + ["O2-"] * 4
1✔
143

144
    def test_methods(self):
1✔
145
        # Test various structure methods
146
        self.zno55.get_primitive_structure()
1✔
147

148
    def test_as_from_dict(self):
1✔
149
        d = self.zno55.as_dict()
1✔
150
        obj = Slab.from_dict(d)
1✔
151
        assert obj.miller_index == (1, 0, 0)
1✔
152

153
    def test_dipole_and_is_polar(self):
1✔
154
        self.assertArrayAlmostEqual(self.zno55.dipole, [0, 0, 0])
1✔
155
        assert not self.zno55.is_polar()
1✔
156
        cscl = self.get_structure("CsCl")
1✔
157
        cscl.add_oxidation_state_by_element({"Cs": 1, "Cl": -1})
1✔
158
        slab = SlabGenerator(
1✔
159
            cscl,
160
            [1, 0, 0],
161
            5,
162
            5,
163
            reorient_lattice=False,
164
            lll_reduce=False,
165
            center_slab=False,
166
        ).get_slab()
167
        self.assertArrayAlmostEqual(slab.dipole, [-4.209, 0, 0])
1✔
168
        assert slab.is_polar()
1✔
169

170
    def test_surface_sites_and_symmetry(self):
1✔
171
        # test if surfaces are equivalent by using
172
        # Laue symmetry and surface site equivalence
173

174
        for bool in [True, False]:
1✔
175
            # We will also set the slab to be centered and
176
            # off centered in order to test the center of mass
177
            slabgen = SlabGenerator(self.agfcc, (3, 1, 0), 10, 10, center_slab=bool)
1✔
178
            slab = slabgen.get_slabs()[0]
1✔
179
            surf_sites_dict = slab.get_surface_sites()
1✔
180
            assert len(surf_sites_dict["top"]) == len(surf_sites_dict["bottom"])
1✔
181
            total_surf_sites = sum(len(surf_sites_dict[key]) for key in surf_sites_dict)
1✔
182
            assert slab.is_symmetric()
1✔
183
            assert total_surf_sites / 2 == 4
1✔
184

185
            # Test if the ratio of surface sites per area is
186
            # constant, ie are the surface energies the same
187
            r1 = total_surf_sites / (2 * slab.surface_area)
1✔
188
            slabgen = SlabGenerator(self.agfcc, (3, 1, 0), 10, 10, primitive=False)
1✔
189
            slab = slabgen.get_slabs()[0]
1✔
190
            surf_sites_dict = slab.get_surface_sites()
1✔
191
            total_surf_sites = sum(len(surf_sites_dict[key]) for key in surf_sites_dict)
1✔
192
            r2 = total_surf_sites / (2 * slab.surface_area)
1✔
193
            self.assertArrayAlmostEqual(r1, r2)
1✔
194

195
    def test_symmetrization(self):
1✔
196
        # Restricted to primitive_elemental materials due to the risk of
197
        # broken stoichiometry. For compound materials, use is_polar()
198

199
        # Get all slabs for P6/mmm Ti and Fm-3m Ag up to index of 2
200

201
        all_Ti_slabs = generate_all_slabs(
1✔
202
            self.ti,
203
            2,
204
            10,
205
            10,
206
            bonds=None,
207
            tol=1e-3,
208
            max_broken_bonds=0,
209
            lll_reduce=False,
210
            center_slab=False,
211
            primitive=True,
212
            max_normal_search=2,
213
            symmetrize=True,
214
        )
215

216
        all_Ag_fcc_slabs = generate_all_slabs(
1✔
217
            self.agfcc,
218
            2,
219
            10,
220
            10,
221
            bonds=None,
222
            tol=1e-3,
223
            max_broken_bonds=0,
224
            lll_reduce=False,
225
            center_slab=False,
226
            primitive=True,
227
            max_normal_search=2,
228
            symmetrize=True,
229
        )
230

231
        all_slabs = [all_Ti_slabs, all_Ag_fcc_slabs]
1✔
232

233
        for slabs in all_slabs:
1✔
234
            asymmetric_count = 0
1✔
235
            symmetric_count = 0
1✔
236

237
            for slab in slabs:
1✔
238
                sg = SpacegroupAnalyzer(slab)
1✔
239

240
                # Check if a slab is symmetric
241
                if not sg.is_laue():
1✔
242
                    asymmetric_count += 1
×
243
                else:
244
                    symmetric_count += 1
1✔
245

246
            # Check if slabs are all symmetric
247
            assert asymmetric_count == 0
1✔
248
            assert symmetric_count == len(slabs)
1✔
249

250
        # Check if we can generate symmetric slabs from bulk with no inversion
251
        all_non_laue_slabs = generate_all_slabs(self.nonlaue, 1, 15, 15, symmetrize=True)
1✔
252
        assert len(all_non_laue_slabs) > 0
1✔
253

254
    def test_get_symmetric_sites(self):
1✔
255
        # Check to see if we get an equivalent site on one
256
        # surface if we add a new site to the other surface
257

258
        all_Ti_slabs = generate_all_slabs(
1✔
259
            self.ti,
260
            2,
261
            10,
262
            10,
263
            bonds=None,
264
            tol=1e-3,
265
            max_broken_bonds=0,
266
            lll_reduce=False,
267
            center_slab=False,
268
            primitive=True,
269
            max_normal_search=2,
270
            symmetrize=True,
271
        )
272

273
        for slab in all_Ti_slabs:
1✔
274
            sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2])
1✔
275
            site = sorted_sites[-1]
1✔
276
            point = np.array(site.frac_coords)
1✔
277
            point[2] = point[2] + 0.1
1✔
278
            point2 = slab.get_symmetric_site(point)
1✔
279
            slab.append("O", point)
1✔
280
            slab.append("O", point2)
1✔
281

282
            # Check if slab is all symmetric
283
            sg = SpacegroupAnalyzer(slab)
1✔
284
            assert sg.is_laue()
1✔
285

286
    def test_oriented_unit_cell(self):
1✔
287
        # Check to see if we get the fully reduced oriented unit
288
        # cell. This will also ensure that the constrain_latt
289
        # parameter for get_primitive_structure is working properly
290

291
        def surface_area(s):
1✔
292
            m = s.lattice.matrix
1✔
293
            return np.linalg.norm(np.cross(m[0], m[1]))
1✔
294

295
        all_slabs = generate_all_slabs(self.agfcc, 2, 10, 10, max_normal_search=3)
1✔
296
        for slab in all_slabs:
1✔
297
            ouc = slab.oriented_unit_cell
1✔
298

299
            assert round(abs(surface_area(slab) - surface_area(ouc)), 7) == 0
1✔
300
            assert len(slab) >= len(ouc)
1✔
301

302
    def test_get_slab_regions(self):
1✔
303
        # If a slab layer in the slab cell is not completely inside
304
        # the cell (noncontiguous), check that get_slab_regions will
305
        # be able to identify where the slab layers are located
306

307
        s = self.get_structure("LiFePO4")
1✔
308
        slabgen = SlabGenerator(s, (0, 0, 1), 15, 15)
1✔
309
        slab = slabgen.get_slabs()[0]
1✔
310
        slab.translate_sites([i for i, site in enumerate(slab)], [0, 0, -0.25])
1✔
311
        bottom_c, top_c = [], []
1✔
312
        for site in slab:
1✔
313
            if site.frac_coords[2] < 0.5:
1✔
314
                bottom_c.append(site.frac_coords[2])
1✔
315
            else:
316
                top_c.append(site.frac_coords[2])
1✔
317
        ranges = get_slab_regions(slab)
1✔
318
        assert tuple(ranges[0]) == (0, max(bottom_c))
1✔
319
        assert tuple(ranges[1]) == (min(top_c), 1)
1✔
320

321
    def test_as_dict(self):
1✔
322
        slabs = generate_all_slabs(
1✔
323
            self.ti,
324
            1,
325
            10,
326
            10,
327
            bonds=None,
328
            tol=1e-3,
329
            max_broken_bonds=0,
330
            lll_reduce=False,
331
            center_slab=False,
332
            primitive=True,
333
        )
334
        slab = slabs[0]
1✔
335
        s = json.dumps(slab.as_dict())
1✔
336
        d = json.loads(s)
1✔
337
        assert slab == Slab.from_dict(d)
1✔
338

339
        # test initialising with a list scale_factor
340
        slab = Slab(
1✔
341
            self.zno55.lattice,
342
            self.zno55.species,
343
            self.zno55.frac_coords,
344
            self.zno55.miller_index,
345
            self.zno55.oriented_unit_cell,
346
            0,
347
            self.zno55.scale_factor.tolist(),
348
        )
349
        s = json.dumps(slab.as_dict())
1✔
350
        d = json.loads(s)
1✔
351
        assert slab == Slab.from_dict(d)
1✔
352

353

354
class SlabGeneratorTest(PymatgenTest):
1✔
355
    def setUp(self):
1✔
356
        lattice = Lattice.cubic(3.010)
1✔
357
        frac_coords = [
1✔
358
            [0.00000, 0.00000, 0.00000],
359
            [0.00000, 0.50000, 0.50000],
360
            [0.50000, 0.00000, 0.50000],
361
            [0.50000, 0.50000, 0.00000],
362
            [0.50000, 0.00000, 0.00000],
363
            [0.50000, 0.50000, 0.50000],
364
            [0.00000, 0.00000, 0.50000],
365
            [0.00000, 0.50000, 0.00000],
366
        ]
367
        species = ["Mg", "Mg", "Mg", "Mg", "O", "O", "O", "O"]
1✔
368
        self.MgO = Structure(lattice, species, frac_coords)
1✔
369
        self.MgO.add_oxidation_state_by_element({"Mg": 2, "O": -6})
1✔
370

371
        lattice_Dy = Lattice.hexagonal(3.58, 25.61)
1✔
372
        frac_coords_Dy = [
1✔
373
            [0.00000, 0.00000, 0.00000],
374
            [0.66667, 0.33333, 0.11133],
375
            [0.00000, 0.00000, 0.222],
376
            [0.66667, 0.33333, 0.33333],
377
            [0.33333, 0.66666, 0.44467],
378
            [0.66667, 0.33333, 0.55533],
379
            [0.33333, 0.66667, 0.66667],
380
            [0.00000, 0.00000, 0.778],
381
            [0.33333, 0.66667, 0.88867],
382
        ]
383
        species_Dy = ["Dy", "Dy", "Dy", "Dy", "Dy", "Dy", "Dy", "Dy", "Dy"]
1✔
384
        self.Dy = Structure(lattice_Dy, species_Dy, frac_coords_Dy)
1✔
385

386
    def test_get_slab(self):
1✔
387
        s = self.get_structure("LiFePO4")
1✔
388
        gen = SlabGenerator(s, [0, 0, 1], 10, 10)
1✔
389
        s = gen.get_slab(0.25)
1✔
390
        assert round(abs(s.lattice.abc[2] - 20.820740000000001), 7) == 0
1✔
391

392
        fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
393
        gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, max_normal_search=1)
1✔
394
        slab = gen.get_slab()
1✔
395
        assert len(slab) == 6
1✔
396
        gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, primitive=False, max_normal_search=1)
1✔
397
        slab_non_prim = gen.get_slab()
1✔
398
        assert len(slab_non_prim) == len(slab) * 4
1✔
399

400
        # Some randomized testing of cell vectors
401
        for i in range(1, 231):
1✔
402
            i = random.randint(1, 230)
1✔
403
            sg = SpaceGroup.from_int_number(i)
1✔
404
            if sg.crystal_system == "hexagonal" or (
1✔
405
                sg.crystal_system == "trigonal"
406
                and (
407
                    sg.symbol.endswith("H")
408
                    or sg.int_number
409
                    in [
410
                        143,
411
                        144,
412
                        145,
413
                        147,
414
                        149,
415
                        150,
416
                        151,
417
                        152,
418
                        153,
419
                        154,
420
                        156,
421
                        157,
422
                        158,
423
                        159,
424
                        162,
425
                        163,
426
                        164,
427
                        165,
428
                    ]
429
                )
430
            ):
431
                latt = Lattice.hexagonal(5, 10)
1✔
432
            else:
433
                # Cubic lattice is compatible with all other space groups.
434
                latt = Lattice.cubic(5)
1✔
435
            s = Structure.from_spacegroup(i, latt, ["H"], [[0, 0, 0]])
1✔
436
            miller = (0, 0, 0)
1✔
437
            while miller == (0, 0, 0):
1✔
438
                miller = (
1✔
439
                    random.randint(0, 6),
440
                    random.randint(0, 6),
441
                    random.randint(0, 6),
442
                )
443
            gen = SlabGenerator(s, miller, 10, 10)
1✔
444
            a, b, c = gen.oriented_unit_cell.lattice.matrix
1✔
445
            assert round(abs(np.dot(a, gen._normal) - 0), 7) == 0
1✔
446
            assert round(abs(np.dot(b, gen._normal) - 0), 7) == 0
1✔
447

448
    def test_normal_search(self):
1✔
449
        fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
450
        for miller in [(1, 0, 0), (1, 1, 0), (1, 1, 1), (2, 1, 1)]:
1✔
451
            gen = SlabGenerator(fcc, miller, 10, 10)
1✔
452
            gen_normal = SlabGenerator(fcc, miller, 10, 10, max_normal_search=max(miller))
1✔
453
            slab = gen_normal.get_slab()
1✔
454
            assert round(abs(slab.lattice.alpha - 90), 7) == 0
1✔
455
            assert round(abs(slab.lattice.beta - 90), 7) == 0
1✔
456
            assert len(gen_normal.oriented_unit_cell) >= len(gen.oriented_unit_cell)
1✔
457

458
        graphite = self.get_structure("Graphite")
1✔
459
        for miller in [(1, 0, 0), (1, 1, 0), (0, 0, 1), (2, 1, 1)]:
1✔
460
            gen = SlabGenerator(graphite, miller, 10, 10)
1✔
461
            gen_normal = SlabGenerator(graphite, miller, 10, 10, max_normal_search=max(miller))
1✔
462
            assert len(gen_normal.oriented_unit_cell) >= len(gen.oriented_unit_cell)
1✔
463

464
        sc = Structure(
1✔
465
            Lattice.hexagonal(3.32, 5.15),
466
            ["Sc", "Sc"],
467
            [[1 / 3, 2 / 3, 0.25], [2 / 3, 1 / 3, 0.75]],
468
        )
469
        gen = SlabGenerator(sc, (1, 1, 1), 10, 10, max_normal_search=1)
1✔
470
        assert round(abs(gen.oriented_unit_cell.lattice.angles[1] - 90), 7) == 0
1✔
471

472
    def test_get_slabs(self):
1✔
473
        gen = SlabGenerator(self.get_structure("CsCl"), [0, 0, 1], 10, 10)
1✔
474

475
        # Test orthogonality of some internal variables.
476
        a, b, c = gen.oriented_unit_cell.lattice.matrix
1✔
477
        assert round(abs(np.dot(a, gen._normal) - 0), 7) == 0
1✔
478
        assert round(abs(np.dot(b, gen._normal) - 0), 7) == 0
1✔
479

480
        assert len(gen.get_slabs()) == 1
1✔
481

482
        s = self.get_structure("LiFePO4")
1✔
483
        gen = SlabGenerator(s, [0, 0, 1], 10, 10)
1✔
484
        assert len(gen.get_slabs()) == 5
1✔
485

486
        assert len(gen.get_slabs(bonds={("P", "O"): 3})) == 2
1✔
487

488
        # There are no slabs in LFP that does not break either P-O or Fe-O
489
        # bonds for a miller index of [0, 0, 1].
490
        assert len(gen.get_slabs(bonds={("P", "O"): 3, ("Fe", "O"): 3})) == 0
1✔
491

492
        # If we allow some broken bonds, there are a few slabs.
493
        assert len(gen.get_slabs(bonds={("P", "O"): 3, ("Fe", "O"): 3}, max_broken_bonds=2)) == 2
1✔
494

495
        # At this threshold, only the origin and center Li results in
496
        # clustering. All other sites are non-clustered. So the of
497
        # slabs is of sites in LiFePO4 unit cell - 2 + 1.
498
        assert len(gen.get_slabs(tol=1e-4, ftol=1e-4)) == 15
1✔
499

500
        LiCoO2 = Structure.from_file(get_path("icsd_LiCoO2.cif"), primitive=False)
1✔
501
        gen = SlabGenerator(LiCoO2, [0, 0, 1], 10, 10)
1✔
502
        lco = gen.get_slabs(bonds={("Co", "O"): 3})
1✔
503
        assert len(lco) == 1
1✔
504
        a, b, c = gen.oriented_unit_cell.lattice.matrix
1✔
505
        assert round(abs(np.dot(a, gen._normal) - 0), 7) == 0
1✔
506
        assert round(abs(np.dot(b, gen._normal) - 0), 7) == 0
1✔
507

508
        scc = Structure.from_spacegroup("Pm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
509
        gen = SlabGenerator(scc, [0, 0, 1], 10, 10)
1✔
510
        slabs = gen.get_slabs()
1✔
511
        assert len(slabs) == 1
1✔
512
        gen = SlabGenerator(scc, [1, 1, 1], 10, 10, max_normal_search=1)
1✔
513
        slabs = gen.get_slabs()
1✔
514
        assert len(slabs) == 1
1✔
515

516
        # Test whether using units of hkl planes instead of Angstroms for
517
        # min_slab_size and min_vac_size will give us the same number of atoms
518
        natoms = []
1✔
519
        for a in [1, 1.4, 2.5, 3.6]:
1✔
520
            s = Structure.from_spacegroup("Im-3m", Lattice.cubic(a), ["Fe"], [[0, 0, 0]])
1✔
521
            slabgen = SlabGenerator(s, (1, 1, 1), 10, 10, in_unit_planes=True, max_normal_search=2)
1✔
522
            natoms.append(len(slabgen.get_slab()))
1✔
523
        n = natoms[0]
1✔
524
        for i in natoms:
1✔
525
            assert n == i
1✔
526

527
    def test_triclinic_TeI(self):
1✔
528
        # Test case for a triclinic structure of TeI. Only these three
529
        # Miller indices are used because it is easier to identify which
530
        # atoms should be in a surface together. The closeness of the sites
531
        # in other Miller indices can cause some ambiguity when choosing a
532
        # higher tolerance.
533
        numb_slabs = {(0, 0, 1): 5, (0, 1, 0): 3, (1, 0, 0): 7}
1✔
534
        TeI = Structure.from_file(get_path("icsd_TeI.cif"), primitive=False)
1✔
535
        for k, v in numb_slabs.items():
1✔
536
            trclnc_TeI = SlabGenerator(TeI, k, 10, 10)
1✔
537
            TeI_slabs = trclnc_TeI.get_slabs()
1✔
538
            assert v == len(TeI_slabs)
1✔
539

540
    def test_get_orthogonal_c_slab(self):
1✔
541
        TeI = Structure.from_file(get_path("icsd_TeI.cif"), primitive=False)
1✔
542
        trclnc_TeI = SlabGenerator(TeI, (0, 0, 1), 10, 10)
1✔
543
        TeI_slabs = trclnc_TeI.get_slabs()
1✔
544
        slab = TeI_slabs[0]
1✔
545
        norm_slab = slab.get_orthogonal_c_slab()
1✔
546
        assert round(abs(norm_slab.lattice.angles[0] - 90), 7) == 0
1✔
547
        assert round(abs(norm_slab.lattice.angles[1] - 90), 7) == 0
1✔
548

549
    def test_get_orthogonal_c_slab_site_props(self):
1✔
550
        TeI = Structure.from_file(get_path("icsd_TeI.cif"), primitive=False)
1✔
551
        trclnc_TeI = SlabGenerator(TeI, (0, 0, 1), 10, 10)
1✔
552
        TeI_slabs = trclnc_TeI.get_slabs()
1✔
553
        slab = TeI_slabs[0]
1✔
554
        # Add site property to slab
555
        sd_list = [[True, True, True] for site in slab.sites]
1✔
556
        new_sp = slab.site_properties
1✔
557
        new_sp["selective_dynamics"] = sd_list
1✔
558
        slab_with_site_props = slab.copy(site_properties=new_sp)
1✔
559

560
        # Get orthogonal slab
561
        norm_slab = slab_with_site_props.get_orthogonal_c_slab()
1✔
562

563
        # Check if site properties is consistent (or kept)
564
        assert slab_with_site_props.site_properties == norm_slab.site_properties
1✔
565

566
    def test_get_tasker2_slabs(self):
1✔
567
        # The uneven distribution of ions on the (111) facets of Halite type
568
        # slabs are typical examples of Tasker 3 structures. We will test
569
        # this algo to generate a Tasker 2 structure instead
570
        slabgen = SlabGenerator(self.MgO, (1, 1, 1), 10, 10, max_normal_search=1)
1✔
571
        # We generate the Tasker 3 structure first
572
        slab = slabgen.get_slabs()[0]
1✔
573
        assert not slab.is_symmetric()
1✔
574
        assert slab.is_polar()
1✔
575
        # Now to generate the Tasker 2 structure, we must
576
        # ensure there are enough ions on top to move around
577
        slab.make_supercell([2, 1, 1])
1✔
578
        slabs = slab.get_tasker2_slabs()
1✔
579
        # Check if our Tasker 2 slab is nonpolar and symmetric
580
        for slab in slabs:
1✔
581
            assert slab.is_symmetric()
1✔
582
            assert not slab.is_polar()
1✔
583

584
    def test_nonstoichiometric_symmetrized_slab(self):
1✔
585
        # For the (111) halite slab, sometimes a non-stoichiometric
586
        # system is preferred over the stoichiometric Tasker 2.
587
        slabgen = SlabGenerator(self.MgO, (1, 1, 1), 10, 10, max_normal_search=1)
1✔
588
        slabs = slabgen.get_slabs(symmetrize=True)
1✔
589

590
        # We should end up with two terminations, one with
591
        # an Mg rich surface and another O rich surface
592
        assert len(slabs) == 2
1✔
593
        for slab in slabs:
1✔
594
            assert slab.is_symmetric()
1✔
595

596
        # For a low symmetry primitive_elemental system such as
597
        # R-3m, there should be some non-symmetric slabs
598
        # without using non-stoichiometric_symmetrized_slab
599
        slabs = generate_all_slabs(self.Dy, 1, 30, 30, center_slab=True, symmetrize=True)
1✔
600
        for s in slabs:
1✔
601
            assert s.is_symmetric()
1✔
602
            assert len(s) > len(self.Dy)
1✔
603

604
    def test_move_to_other_side(self):
1✔
605
        # Tests to see if sites are added to opposite side
606
        s = self.get_structure("LiFePO4")
1✔
607
        slabgen = SlabGenerator(s, (0, 0, 1), 10, 10, center_slab=True)
1✔
608
        slab = slabgen.get_slab()
1✔
609
        surface_sites = slab.get_surface_sites()
1✔
610

611
        # check if top sites are moved to the bottom
612
        top_index = [ss[1] for ss in surface_sites["top"]]
1✔
613
        slab = slabgen.move_to_other_side(slab, top_index)
1✔
614
        all_bottom = [slab[i].frac_coords[2] < slab.center_of_mass[2] for i in top_index]
1✔
615
        assert all(all_bottom)
1✔
616

617
        # check if bottom sites are moved to the top
618
        bottom_index = [ss[1] for ss in surface_sites["bottom"]]
1✔
619
        slab = slabgen.move_to_other_side(slab, bottom_index)
1✔
620
        all_top = [slab[i].frac_coords[2] > slab.center_of_mass[2] for i in bottom_index]
1✔
621
        assert all(all_top)
1✔
622

623
    def test_bonds_broken(self):
1✔
624
        # Querying the Materials Project database for Si
625
        s = self.get_structure("Si")
1✔
626
        # Conventional unit cell is supplied to ensure miller indices
627
        # correspond to usual crystallographic definitions
628
        conv_bulk = SpacegroupAnalyzer(s).get_conventional_standard_structure()
1✔
629
        slabgen = SlabGenerator(conv_bulk, [1, 1, 1], 10, 10, center_slab=True)
1✔
630
        # Setting a generous estimate for max_broken_bonds
631
        # so that all terminations are generated. These slabs
632
        # are ordered by ascending number of bonds broken
633
        # which is assigned to Slab.energy
634
        slabs = slabgen.get_slabs(bonds={("Si", "Si"): 2.40}, max_broken_bonds=30)
1✔
635
        # Looking at the two slabs generated in VESTA, we
636
        # expect 2 and 6 bonds broken so we check for this.
637
        # Number of broken bonds are floats due to primitive
638
        # flag check and subsequent transformation of slabs.
639
        assert slabs[0].energy, 2.0
1✔
640
        assert slabs[1].energy, 6.0
1✔
641

642

643
class ReconstructionGeneratorTests(PymatgenTest):
1✔
644
    def setUp(self):
1✔
645
        l = Lattice.cubic(3.51)
1✔
646
        species = ["Ni"]
1✔
647
        coords = [[0, 0, 0]]
1✔
648
        self.Ni = Structure.from_spacegroup("Fm-3m", l, species, coords)
1✔
649
        l = Lattice.cubic(2.819000)
1✔
650
        species = ["Fe"]
1✔
651
        coords = [[0, 0, 0]]
1✔
652
        self.Fe = Structure.from_spacegroup("Im-3m", l, species, coords)
1✔
653
        self.Si = Structure.from_spacegroup("Fd-3m", Lattice.cubic(5.430500), ["Si"], [(0, 0, 0.5)])
1✔
654

655
        with open(
1✔
656
            os.path.join(
657
                os.path.abspath(os.path.dirname(__file__)),
658
                "..",
659
                "reconstructions_archive.json",
660
            )
661
        ) as data_file:
662
            self.rec_archive = json.load(data_file)
1✔
663

664
    def test_build_slab(self):
1✔
665
        # First lets test a reconstruction where we only remove atoms
666
        recon = ReconstructionGenerator(self.Ni, 10, 10, "fcc_110_missing_row_1x2")
1✔
667
        slab = recon.get_unreconstructed_slabs()[0]
1✔
668
        recon_slab = recon.build_slabs()[0]
1✔
669
        assert recon_slab.reconstruction
1✔
670
        assert len(slab) == len(recon_slab) + 2
1✔
671
        assert recon_slab.is_symmetric()
1✔
672

673
        # Test if the ouc corresponds to the reconstructed slab
674
        recon_ouc = recon_slab.oriented_unit_cell
1✔
675
        ouc = slab.oriented_unit_cell
1✔
676
        assert ouc.lattice.b * 2 == recon_ouc.lattice.b
1✔
677
        assert len(ouc) * 2 == len(recon_ouc)
1✔
678

679
        # Test a reconstruction where we simply add atoms
680
        recon = ReconstructionGenerator(self.Ni, 10, 10, "fcc_111_adatom_t_1x1")
1✔
681
        slab = recon.get_unreconstructed_slabs()[0]
1✔
682
        recon_slab = recon.build_slabs()[0]
1✔
683
        assert len(slab) == len(recon_slab) - 2
1✔
684
        assert recon_slab.is_symmetric()
1✔
685

686
        # If a slab references another slab,
687
        # make sure it is properly generated
688
        recon = ReconstructionGenerator(self.Ni, 10, 10, "fcc_111_adatom_ft_1x1")
1✔
689
        slab = recon.build_slabs()[0]
1✔
690
        assert slab.is_symmetric
1✔
691

692
        # Test a reconstruction where it works on a specific
693
        # termination (Fd-3m (111))
694
        recon = ReconstructionGenerator(self.Si, 10, 10, "diamond_111_1x2")
1✔
695
        slab = recon.get_unreconstructed_slabs()[0]
1✔
696
        recon_slab = recon.build_slabs()[0]
1✔
697
        assert len(slab) == len(recon_slab) - 8
1✔
698
        assert recon_slab.is_symmetric()
1✔
699

700
        # Test a reconstruction where terminations give
701
        # different reconstructions with a non-primitive_elemental system
702

703
    def test_get_d(self):
1✔
704
        # Ensure that regardless of the size of the vacuum or slab
705
        # layer, the spacing between atomic layers should be the same
706

707
        recon = ReconstructionGenerator(self.Si, 10, 10, "diamond_100_2x1")
1✔
708

709
        recon2 = ReconstructionGenerator(self.Si, 20, 10, "diamond_100_2x1")
1✔
710
        s1 = recon.get_unreconstructed_slabs()[0]
1✔
711
        s2 = recon2.get_unreconstructed_slabs()[0]
1✔
712
        assert round(abs(get_d(s1) - get_d(s2)), 7) == 0
1✔
713

714
    @unittest.skip("This test relies on neighbor orders and is hard coded. Disable temporarily")
1✔
715
    def test_previous_reconstructions(self):
1✔
716
        # Test to see if we generated all reconstruction types correctly and nothing changes
717

718
        m = StructureMatcher()
×
719
        for n in self.rec_archive:
×
720
            if "base_reconstruction" in self.rec_archive[n]:
×
721
                arch = self.rec_archive[self.rec_archive[n]["base_reconstruction"]]
×
722
                sg = arch["spacegroup"]["symbol"]
×
723
            else:
724
                sg = self.rec_archive[n]["spacegroup"]["symbol"]
×
725
            if sg == "Fm-3m":
×
726
                rec = ReconstructionGenerator(self.Ni, 20, 20, n)
×
727
                el = self.Ni[0].species_string
×
728
            elif sg == "Im-3m":
×
729
                rec = ReconstructionGenerator(self.Fe, 20, 20, n)
×
730
                el = self.Fe[0].species_string
×
731
            elif sg == "Fd-3m":
×
732
                rec = ReconstructionGenerator(self.Si, 20, 20, n)
×
733
                el = self.Si[0].species_string
×
734

735
            slabs = rec.build_slabs()
×
736
            s = Structure.from_file(get_path(os.path.join("reconstructions", el + "_" + n + ".cif")))
×
737
            assert any([len(m.group_structures([s, slab])) == 1 for slab in slabs])
×
738

739

740
class MillerIndexFinderTests(PymatgenTest):
1✔
741
    def setUp(self):
1✔
742
        self.cscl = Structure.from_spacegroup("Pm-3m", Lattice.cubic(4.2), ["Cs", "Cl"], [[0, 0, 0], [0.5, 0.5, 0.5]])
1✔
743
        self.Fe = Structure.from_spacegroup("Im-3m", Lattice.cubic(2.82), ["Fe"], [[0, 0, 0]])
1✔
744
        mglatt = Lattice.from_parameters(3.2, 3.2, 5.13, 90, 90, 120)
1✔
745
        self.Mg = Structure(mglatt, ["Mg", "Mg"], [[1 / 3, 2 / 3, 1 / 4], [2 / 3, 1 / 3, 3 / 4]])
1✔
746
        self.lifepo4 = self.get_structure("LiFePO4")
1✔
747
        self.tei = Structure.from_file(get_path("icsd_TeI.cif"), primitive=False)
1✔
748
        self.LiCoO2 = Structure.from_file(get_path("icsd_LiCoO2.cif"), primitive=False)
1✔
749

750
        self.p1 = Structure(
1✔
751
            Lattice.from_parameters(3, 4, 5, 31, 43, 50),
752
            ["H", "He"],
753
            [[0, 0, 0], [0.1, 0.2, 0.3]],
754
        )
755
        self.graphite = self.get_structure("Graphite")
1✔
756
        self.trigBi = Structure(
1✔
757
            Lattice.from_parameters(3, 3, 10, 90, 90, 120),
758
            ["Bi", "Bi", "Bi", "Bi", "Bi", "Bi"],
759
            [
760
                [0.3333, 0.6666, 0.39945113],
761
                [0.0000, 0.0000, 0.26721554],
762
                [0.0000, 0.0000, 0.73278446],
763
                [0.6666, 0.3333, 0.60054887],
764
                [0.6666, 0.3333, 0.06611779],
765
                [0.3333, 0.6666, 0.93388221],
766
            ],
767
        )
768

769
    def test_get_symmetrically_distinct_miller_indices(self):
1✔
770
        # Tests to see if the function obtains the known number of unique slabs
771

772
        indices = get_symmetrically_distinct_miller_indices(self.cscl, 1)
1✔
773
        assert len(indices) == 3
1✔
774
        indices = get_symmetrically_distinct_miller_indices(self.cscl, 2)
1✔
775
        assert len(indices) == 6
1✔
776

777
        assert len(get_symmetrically_distinct_miller_indices(self.lifepo4, 1)) == 7
1✔
778

779
        # The TeI P-1 structure should have 13 unique millers (only inversion
780
        # symmetry eliminates pairs)
781
        indices = get_symmetrically_distinct_miller_indices(self.tei, 1)
1✔
782
        assert len(indices) == 13
1✔
783

784
        # P1 and P-1 should have the same # of miller indices since surfaces
785
        # always have inversion symmetry.
786
        indices = get_symmetrically_distinct_miller_indices(self.p1, 1)
1✔
787
        assert len(indices) == 13
1✔
788

789
        indices = get_symmetrically_distinct_miller_indices(self.graphite, 2)
1✔
790
        assert len(indices) == 12
1✔
791

792
        # Now try a trigonal system.
793
        indices = get_symmetrically_distinct_miller_indices(self.trigBi, 2, return_hkil=True)
1✔
794
        assert len(indices) == 17
1✔
795
        assert all([len(hkl) == 4 for hkl in indices])
1✔
796

797
    def test_get_symmetrically_equivalent_miller_indices(self):
1✔
798
        # Tests to see if the function obtains all equivalent hkl for cubic (100)
799
        indices001 = [
1✔
800
            (1, 0, 0),
801
            (0, 1, 0),
802
            (0, 0, 1),
803
            (0, 0, -1),
804
            (0, -1, 0),
805
            (-1, 0, 0),
806
        ]
807
        indices = get_symmetrically_equivalent_miller_indices(self.cscl, (1, 0, 0))
1✔
808
        assert all([hkl in indices for hkl in indices001])
1✔
809

810
        # Tests to see if it captures expanded Miller indices in the family e.g. (001) == (002)
811
        hcp_indices_100 = get_symmetrically_equivalent_miller_indices(self.Mg, (1, 0, 0))
1✔
812
        hcp_indices_200 = get_symmetrically_equivalent_miller_indices(self.Mg, (2, 0, 0))
1✔
813
        assert len(hcp_indices_100) * 2 == len(hcp_indices_200)
1✔
814
        assert len(hcp_indices_100) == 6
1✔
815
        assert all([len(hkl) == 4 for hkl in hcp_indices_100])
1✔
816

817
    def test_generate_all_slabs(self):
1✔
818
        slabs = generate_all_slabs(self.cscl, 1, 10, 10)
1✔
819
        # Only three possible slabs, one each in (100), (110) and (111).
820
        assert len(slabs) == 3
1✔
821

822
        # make sure it generates reconstructions
823
        slabs = generate_all_slabs(self.Fe, 1, 10, 10, include_reconstructions=True)
1✔
824

825
        # Four possible slabs, (100), (110), (111) and the zigzag (100).
826
        assert len(slabs) == 4
1✔
827

828
        slabs = generate_all_slabs(self.cscl, 1, 10, 10, bonds={("Cs", "Cl"): 4})
1✔
829
        # No slabs if we don't allow broken Cs-Cl
830
        assert len(slabs) == 0
1✔
831

832
        slabs = generate_all_slabs(self.cscl, 1, 10, 10, bonds={("Cs", "Cl"): 4}, max_broken_bonds=100)
1✔
833
        assert len(slabs) == 3
1✔
834

835
        slabs2 = generate_all_slabs(self.lifepo4, 1, 10, 10, bonds={("P", "O"): 3, ("Fe", "O"): 3})
1✔
836
        assert len(slabs2) == 0
1✔
837

838
        # There should be only one possible stable surfaces, all of which are
839
        # in the (001) oriented unit cell
840
        slabs3 = generate_all_slabs(self.LiCoO2, 1, 10, 10, bonds={("Co", "O"): 3})
1✔
841
        assert len(slabs3) == 1
1✔
842
        mill = (0, 0, 1)
1✔
843
        for s in slabs3:
1✔
844
            assert s.miller_index == mill
1✔
845

846
        slabs1 = generate_all_slabs(self.lifepo4, 1, 10, 10, tol=0.1, bonds={("P", "O"): 3})
1✔
847
        assert len(slabs1) == 4
1✔
848

849
        # Now we test this out for repair_broken_bonds()
850
        slabs1_repair = generate_all_slabs(self.lifepo4, 1, 10, 10, tol=0.1, bonds={("P", "O"): 3}, repair=True)
1✔
851
        assert len(slabs1_repair) > len(slabs1)
1✔
852

853
        # Lets see if there are no broken PO4 polyhedrons
854
        miller_list = get_symmetrically_distinct_miller_indices(self.lifepo4, 1)
1✔
855
        all_miller_list = []
1✔
856
        for slab in slabs1_repair:
1✔
857
            hkl = tuple(slab.miller_index)
1✔
858
            if hkl not in all_miller_list:
1✔
859
                all_miller_list.append(hkl)
1✔
860
            broken = []
1✔
861
            for site in slab:
1✔
862
                if site.species_string == "P":
1✔
863
                    neighbors = slab.get_neighbors(site, 3)
1✔
864
                    cn = 0
1✔
865
                    for nn in neighbors:
1✔
866
                        cn += 1 if nn[0].species_string == "O" else 0
1✔
867
                    broken.append(cn != 4)
1✔
868
            assert not any(broken)
1✔
869

870
        # check if we were able to produce at least one
871
        # termination for each distinct Miller _index
872
        assert len(miller_list) == len(all_miller_list)
1✔
873

874
    def test_miller_index_from_sites(self):
1✔
875
        """Test surface miller index convenience function"""
876

877
        # test on a cubic system
878
        m = Lattice.cubic(1)
1✔
879
        s1 = np.array([0.5, -1.5, 3])
1✔
880
        s2 = np.array([0.5, 3.0, -1.5])
1✔
881
        s3 = np.array([2.5, 1.5, -4.0])
1✔
882
        assert miller_index_from_sites(m, [s1, s2, s3]) == (2, 1, 1)
1✔
883

884
        # test casting from matrix to Lattice
885
        m = [[2.319, -4.01662582, 0.0], [2.319, 4.01662582, 0.0], [0.0, 0.0, 7.252]]
1✔
886

887
        s1 = np.array([2.319, 1.33887527, 6.3455])
1✔
888
        s2 = np.array([1.1595, 0.66943764, 4.5325])
1✔
889
        s3 = np.array([1.1595, 0.66943764, 0.9065])
1✔
890
        hkl = miller_index_from_sites(m, [s1, s2, s3])
1✔
891
        assert hkl == (2, -1, 0)
1✔
892

893

894
if __name__ == "__main__":
1✔
895
    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