• 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

46.15
/pymatgen/transformations/tests/test_advanced_transformations.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 unittest
1✔
9
import warnings
1✔
10
from shutil import which
1✔
11

12
import numpy as np
1✔
13
from monty.serialization import loadfn
1✔
14
from pytest import approx
1✔
15

16
from pymatgen.analysis.energy_models import IsingModel
1✔
17
from pymatgen.analysis.gb.grain import GrainBoundaryGenerator
1✔
18
from pymatgen.core.lattice import Lattice
1✔
19
from pymatgen.core.periodic_table import Species
1✔
20
from pymatgen.core.structure import Molecule, Structure
1✔
21
from pymatgen.core.surface import SlabGenerator
1✔
22
from pymatgen.io.cif import CifParser
1✔
23
from pymatgen.io.vasp.inputs import Poscar
1✔
24
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
25
from pymatgen.transformations.advanced_transformations import (
1✔
26
    AddAdsorbateTransformation,
27
    ChargeBalanceTransformation,
28
    CubicSupercellTransformation,
29
    DisorderOrderedTransformation,
30
    DopingTransformation,
31
    EnumerateStructureTransformation,
32
    GrainBoundaryTransformation,
33
    MagOrderingTransformation,
34
    MagOrderParameterConstraint,
35
    MonteCarloRattleTransformation,
36
    MultipleSubstitutionTransformation,
37
    SlabTransformation,
38
    SQSTransformation,
39
    SubstituteSurfaceSiteTransformation,
40
    SubstitutionPredictorTransformation,
41
    SuperTransformation,
42
    _find_codopant,
43
)
44
from pymatgen.transformations.standard_transformations import (
1✔
45
    AutoOxiStateDecorationTransformation,
46
    OrderDisorderedStructureTransformation,
47
    OxidationStateDecorationTransformation,
48
    SubstitutionTransformation,
49
)
50
from pymatgen.util.testing import PymatgenTest
1✔
51

52
try:
1✔
53
    import hiphive
1✔
54
except ImportError:
1✔
55
    hiphive = None
1✔
56

57

58
try:
1✔
59
    import m3gnet
1✔
60
except ImportError:
×
61
    m3gnet = None
×
62

63

64
def get_table():
1✔
65
    """
66
    Loads a lightweight lambda table for use in unit tests to reduce
67
    initialization time, and make unit tests insensitive to changes in the
68
    default lambda table.
69
    """
70
    data_dir = os.path.join(PymatgenTest.TEST_FILES_DIR, "struct_predictor")
1✔
71
    json_file = os.path.join(data_dir, "test_lambda.json")
1✔
72
    with open(json_file) as f:
1✔
73
        lambda_table = json.load(f)
1✔
74
    return lambda_table
1✔
75

76

77
enum_cmd = which("enum.x") or which("multienum.x")
1✔
78
makestr_cmd = which("makestr.x") or which("makeStr.x") or which("makeStr.py")
1✔
79
mcsqs_cmd = which("mcsqs")
1✔
80
enumlib_present = enum_cmd and makestr_cmd
1✔
81

82

83
class SuperTransformationTest(unittest.TestCase):
1✔
84
    def setUp(self):
1✔
85
        warnings.simplefilter("ignore")
1✔
86

87
    def tearDown(self):
1✔
88
        warnings.simplefilter("default")
1✔
89

90
    def test_apply_transformation(self):
1✔
91
        tl = [
1✔
92
            SubstitutionTransformation({"Li+": "Na+"}),
93
            SubstitutionTransformation({"Li+": "K+"}),
94
        ]
95
        t = SuperTransformation(tl)
1✔
96
        coords = []
1✔
97
        coords.append([0, 0, 0])
1✔
98
        coords.append([0.375, 0.375, 0.375])
1✔
99
        coords.append([0.5, 0.5, 0.5])
1✔
100
        coords.append([0.875, 0.875, 0.875])
1✔
101
        coords.append([0.125, 0.125, 0.125])
1✔
102
        coords.append([0.25, 0.25, 0.25])
1✔
103
        coords.append([0.625, 0.625, 0.625])
1✔
104
        coords.append([0.75, 0.75, 0.75])
1✔
105

106
        lattice = Lattice(
1✔
107
            [
108
                [3.8401979337, 0.00, 0.00],
109
                [1.9200989668, 3.3257101909, 0.00],
110
                [0.00, -2.2171384943, 3.1355090603],
111
            ]
112
        )
113
        struct = Structure(lattice, ["Li+", "Li+", "Li+", "Li+", "Li+", "Li+", "O2-", "O2-"], coords)
1✔
114
        s = t.apply_transformation(struct, return_ranked_list=True)
1✔
115

116
        for s_and_t in s:
1✔
117
            assert s_and_t["transformation"].apply_transformation(struct) == s_and_t["structure"]
1✔
118

119
    @unittest.skipIf(not enumlib_present, "enum_lib not present.")
1✔
120
    def test_apply_transformation_mult(self):
1✔
121
        # Test returning multiple structures from each transformation.
122
        disord = Structure(
×
123
            np.eye(3) * 4.209,
124
            [{"Cs+": 0.5, "K+": 0.5}, "Cl-"],
125
            [[0, 0, 0], [0.5, 0.5, 0.5]],
126
        )
127
        disord.make_supercell([2, 2, 1])
×
128

129
        tl = [
×
130
            EnumerateStructureTransformation(),
131
            OrderDisorderedStructureTransformation(),
132
        ]
133
        t = SuperTransformation(tl, nstructures_per_trans=10)
×
134
        assert len(t.apply_transformation(disord, return_ranked_list=20)) == 8
×
135
        t = SuperTransformation(tl)
×
136
        assert len(t.apply_transformation(disord, return_ranked_list=20)) == 2
×
137

138

139
class MultipleSubstitutionTransformationTest(unittest.TestCase):
1✔
140
    def setUp(self):
1✔
141
        warnings.simplefilter("ignore")
1✔
142

143
    def tearDown(self):
1✔
144
        warnings.simplefilter("default")
1✔
145

146
    def test_apply_transformation(self):
1✔
147
        sub_dict = {1: ["Na", "K"]}
1✔
148
        t = MultipleSubstitutionTransformation("Li+", 0.5, sub_dict, None)
1✔
149
        coords = []
1✔
150
        coords.append([0, 0, 0])
1✔
151
        coords.append([0.75, 0.75, 0.75])
1✔
152
        coords.append([0.5, 0.5, 0.5])
1✔
153
        coords.append([0.25, 0.25, 0.25])
1✔
154
        lattice = Lattice(
1✔
155
            [
156
                [3.8401979337, 0.00, 0.00],
157
                [1.9200989668, 3.3257101909, 0.00],
158
                [0.00, -2.2171384943, 3.1355090603],
159
            ]
160
        )
161
        struct = Structure(lattice, ["Li+", "Li+", "O2-", "O2-"], coords)
1✔
162
        assert len(t.apply_transformation(struct, return_ranked_list=True)) == 2
1✔
163

164

165
class ChargeBalanceTransformationTest(unittest.TestCase):
1✔
166
    def test_apply_transformation(self):
1✔
167
        t = ChargeBalanceTransformation("Li+")
1✔
168
        coords = []
1✔
169
        coords.append([0, 0, 0])
1✔
170
        coords.append([0.375, 0.375, 0.375])
1✔
171
        coords.append([0.5, 0.5, 0.5])
1✔
172
        coords.append([0.875, 0.875, 0.875])
1✔
173
        coords.append([0.125, 0.125, 0.125])
1✔
174
        coords.append([0.25, 0.25, 0.25])
1✔
175
        coords.append([0.625, 0.625, 0.625])
1✔
176
        coords.append([0.75, 0.75, 0.75])
1✔
177

178
        lattice = Lattice(
1✔
179
            [
180
                [3.8401979337, 0.00, 0.00],
181
                [1.9200989668, 3.3257101909, 0.00],
182
                [0.00, -2.2171384943, 3.1355090603],
183
            ]
184
        )
185
        struct = Structure(lattice, ["Li+", "Li+", "Li+", "Li+", "Li+", "Li+", "O2-", "O2-"], coords)
1✔
186
        s = t.apply_transformation(struct)
1✔
187

188
        assert s.charge == approx(0, abs=1e-5)
1✔
189

190

191
@unittest.skipIf(not enumlib_present, "enum_lib not present.")
1✔
192
class EnumerateStructureTransformationTest(unittest.TestCase):
1✔
193
    def setUp(self):
1✔
194
        warnings.simplefilter("ignore")
×
195

196
    def tearDown(self):
1✔
197
        warnings.simplefilter("default")
×
198

199
    def test_apply_transformation(self):
1✔
200
        enum_trans = EnumerateStructureTransformation(refine_structure=True)
×
201
        enum_trans2 = EnumerateStructureTransformation(refine_structure=True, sort_criteria="nsites")
×
202
        p = Poscar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.LiFePO4"), check_for_POTCAR=False)
×
203
        struct = p.structure
×
204
        expected_ans = [1, 3, 1]
×
205
        for i, frac in enumerate([0.25, 0.5, 0.75]):
×
206
            trans = SubstitutionTransformation({"Fe": {"Fe": frac}})
×
207
            s = trans.apply_transformation(struct)
×
208
            oxitrans = OxidationStateDecorationTransformation({"Li": 1, "Fe": 2, "P": 5, "O": -2})
×
209
            s = oxitrans.apply_transformation(s)
×
210
            alls = enum_trans.apply_transformation(s, 100)
×
211
            assert len(alls) == expected_ans[i]
×
212
            assert isinstance(trans.apply_transformation(s), Structure)
×
213
            for ss in alls:
×
214
                assert "energy" in ss
×
215
            alls = enum_trans2.apply_transformation(s, 100)
×
216
            assert len(alls) == expected_ans[i]
×
217
            assert isinstance(trans.apply_transformation(s), Structure)
×
218
            for ss in alls:
×
219
                assert "num_sites" in ss
×
220

221
        # make sure it works for non-oxidation state decorated structure
222
        trans = SubstitutionTransformation({"Fe": {"Fe": 0.5}})
×
223
        s = trans.apply_transformation(struct)
×
224
        alls = enum_trans.apply_transformation(s, 100)
×
225
        assert len(alls) == 3
×
226
        assert isinstance(trans.apply_transformation(s), Structure)
×
227
        for s in alls:
×
228
            assert "energy" not in s
×
229

230
    @unittest.skipIf(m3gnet is None, "m3gnet package not available.")
1✔
231
    def test_m3gnet(self):
1✔
232
        enum_trans = EnumerateStructureTransformation(refine_structure=True, sort_criteria="m3gnet_relax")
×
233
        p = Poscar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.LiFePO4"), check_for_POTCAR=False)
×
234
        struct = p.structure
×
235
        trans = SubstitutionTransformation({"Fe": {"Fe": 0.5, "Mn": 0.5}})
×
236
        s = trans.apply_transformation(struct)
×
237
        alls = enum_trans.apply_transformation(s, 100)
×
238
        assert len(alls) == 3
×
239
        assert isinstance(trans.apply_transformation(s), Structure)
×
240
        for ss in alls:
×
241
            assert "energy" in ss
×
242

243
        # Check ordering of energy/atom
244
        assert alls[0]["energy"] / alls[0]["num_sites"] <= alls[-1]["energy"] / alls[-1]["num_sites"]
×
245

246
    def test_callable_sort_criteria(self):
1✔
247
        from m3gnet.models import Relaxer
×
248

249
        m3gnet_model = Relaxer(optimizer="BFGS")
×
250

251
        def sort_criteria(s):
×
252
            relax_results = m3gnet_model.relax(s)
×
253
            energy = float(relax_results["trajectory"].energies[-1])
×
254
            return relax_results["final_structure"], energy
×
255

256
        enum_trans = EnumerateStructureTransformation(refine_structure=True, sort_criteria=sort_criteria)
×
257
        p = Poscar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.LiFePO4"), check_for_POTCAR=False)
×
258
        struct = p.structure
×
259
        trans = SubstitutionTransformation({"Fe": {"Fe": 0.5, "Mn": 0.5}})
×
260
        s = trans.apply_transformation(struct)
×
261
        alls = enum_trans.apply_transformation(s, 100)
×
262
        assert len(alls) == 3
×
263
        assert isinstance(trans.apply_transformation(s), Structure)
×
264
        for ss in alls:
×
265
            assert "energy" in ss
×
266

267
        # Check ordering of energy/atom
268
        assert alls[0]["energy"] / alls[0]["num_sites"] <= alls[-1]["energy"] / alls[-1]["num_sites"]
×
269

270
    def test_max_disordered_sites(self):
1✔
271
        l = Lattice.cubic(4)
×
272
        s_orig = Structure(
×
273
            l,
274
            [{"Li": 0.2, "Na": 0.2, "K": 0.6}, {"O": 1}],
275
            [[0, 0, 0], [0.5, 0.5, 0.5]],
276
        )
277
        est = EnumerateStructureTransformation(max_cell_size=None, max_disordered_sites=5)
×
278
        dd = est.apply_transformation(s_orig, return_ranked_list=100)
×
279
        assert len(dd) == 9
×
280
        for d in dd:
×
281
            assert len(d["structure"]) == 10
×
282

283
    def test_to_from_dict(self):
1✔
284
        trans = EnumerateStructureTransformation()
×
285
        d = trans.as_dict()
×
286
        trans = EnumerateStructureTransformation.from_dict(d)
×
287
        assert trans.symm_prec == 0.1
×
288

289

290
class SubstitutionPredictorTransformationTest(unittest.TestCase):
1✔
291
    def test_apply_transformation(self):
1✔
292
        t = SubstitutionPredictorTransformation(threshold=1e-3, alpha=-5, lambda_table=get_table())
1✔
293
        coords = []
1✔
294
        coords.append([0, 0, 0])
1✔
295
        coords.append([0.75, 0.75, 0.75])
1✔
296
        coords.append([0.5, 0.5, 0.5])
1✔
297
        lattice = Lattice(
1✔
298
            [
299
                [3.8401979337, 0.00, 0.00],
300
                [1.9200989668, 3.3257101909, 0.00],
301
                [0.00, -2.2171384943, 3.1355090603],
302
            ]
303
        )
304
        struct = Structure(lattice, ["O2-", "Li1+", "Li1+"], coords)
1✔
305

306
        outputs = t.apply_transformation(struct, return_ranked_list=True)
1✔
307
        assert len(outputs) == 4, "incorrect number of structures"
1✔
308

309
    def test_as_dict(self):
1✔
310
        t = SubstitutionPredictorTransformation(threshold=2, alpha=-2, lambda_table=get_table())
1✔
311
        d = t.as_dict()
1✔
312
        t = SubstitutionPredictorTransformation.from_dict(d)
1✔
313
        assert t.threshold == 2, "incorrect threshold passed through dict"
1✔
314
        assert t._substitutor.p.alpha == -2, "incorrect alpha passed through dict"
1✔
315

316

317
@unittest.skipIf(not enumlib_present, "enum_lib not present.")
1✔
318
class MagOrderingTransformationTest(PymatgenTest):
1✔
319
    def setUp(self):
1✔
320
        latt = Lattice.cubic(4.17)
×
321
        species = ["Ni", "O"]
×
322
        coords = [[0, 0, 0], [0.5, 0.5, 0.5]]
×
323
        self.NiO = Structure.from_spacegroup(225, latt, species, coords)
×
324

325
        latt = Lattice([[2.085, 2.085, 0.0], [0.0, -2.085, -2.085], [-2.085, 2.085, -4.17]])
×
326
        species = ["Ni", "Ni", "O", "O"]
×
327
        coords = [[0.5, 0, 0.5], [0, 0, 0], [0.25, 0.5, 0.25], [0.75, 0.5, 0.75]]
×
328
        self.NiO_AFM_111 = Structure(latt, species, coords)
×
329
        self.NiO_AFM_111.add_spin_by_site([-5, 5, 0, 0])
×
330

331
        latt = Lattice([[2.085, 2.085, 0], [0, 0, -4.17], [-2.085, 2.085, 0]])
×
332
        species = ["Ni", "Ni", "O", "O"]
×
333
        coords = [[0.5, 0.5, 0.5], [0, 0, 0], [0, 0.5, 0], [0.5, 0, 0.5]]
×
334
        self.NiO_AFM_001 = Structure(latt, species, coords)
×
335
        self.NiO_AFM_001.add_spin_by_site([-5, 5, 0, 0])
×
336

337
        parser = CifParser(os.path.join(PymatgenTest.TEST_FILES_DIR, "Fe3O4.cif"))
×
338
        self.Fe3O4 = parser.get_structures()[0]
×
339
        trans = AutoOxiStateDecorationTransformation()
×
340
        self.Fe3O4_oxi = trans.apply_transformation(self.Fe3O4)
×
341

342
        parser = CifParser(os.path.join(PymatgenTest.TEST_FILES_DIR, "Li8Fe2NiCoO8.cif"))
×
343
        self.Li8Fe2NiCoO8 = parser.get_structures()[0]
×
344
        self.Li8Fe2NiCoO8.remove_oxidation_states()
×
345
        warnings.simplefilter("ignore")
×
346

347
    def tearDown(self):
1✔
348
        warnings.simplefilter("default")
×
349

350
    def test_apply_transformation(self):
1✔
351
        trans = MagOrderingTransformation({"Fe": 5})
×
352
        p = Poscar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.LiFePO4"), check_for_POTCAR=False)
×
353
        s = p.structure
×
354
        alls = trans.apply_transformation(s, 10)
×
355
        assert len(alls) == 3
×
356
        f = SpacegroupAnalyzer(alls[0]["structure"], 0.1)
×
357
        assert f.get_space_group_number() == 31
×
358

359
        model = IsingModel(5, 5)
×
360
        trans = MagOrderingTransformation({"Fe": 5}, energy_model=model)
×
361
        alls2 = trans.apply_transformation(s, 10)
×
362
        # Ising model with +J penalizes similar neighbor magmom.
363
        assert alls[0]["structure"] != alls2[0]["structure"]
×
364
        assert alls[0]["structure"] == alls2[2]["structure"]
×
365

366
        s = self.get_structure("Li2O")
×
367
        # Li2O doesn't have magnetism of course, but this is to test the
368
        # enumeration.
369
        trans = MagOrderingTransformation({"Li+": 1}, max_cell_size=3)
×
370
        alls = trans.apply_transformation(s, 100)
×
371
        # TODO: check this is correct, unclear what len(alls) should be
372
        assert len(alls) == 12
×
373

374
        trans = MagOrderingTransformation({"Ni": 5})
×
375
        alls = trans.apply_transformation(self.NiO.get_primitive_structure(), return_ranked_list=10)
×
376

377
        self.assertArrayAlmostEqual(self.NiO_AFM_111.lattice.parameters, alls[0]["structure"].lattice.parameters)
×
378
        self.assertArrayAlmostEqual(self.NiO_AFM_001.lattice.parameters, alls[1]["structure"].lattice.parameters)
×
379

380
    def test_ferrimagnetic(self):
1✔
381
        trans = MagOrderingTransformation({"Fe": 5}, order_parameter=0.75, max_cell_size=1)
×
382
        p = Poscar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.LiFePO4"), check_for_POTCAR=False)
×
383
        s = p.structure
×
384
        a = SpacegroupAnalyzer(s, 0.1)
×
385
        s = a.get_refined_structure()
×
386
        alls = trans.apply_transformation(s, 10)
×
387
        assert len(alls) == 1
×
388

389
    def test_as_from_dict(self):
1✔
390
        trans = MagOrderingTransformation({"Fe": 5}, order_parameter=0.75)
×
391
        d = trans.as_dict()
×
392
        # Check json encodability
393
        _ = json.dumps(d)
×
394
        trans = MagOrderingTransformation.from_dict(d)
×
395
        assert trans.mag_species_spin == {"Fe": 5}
×
396
        from pymatgen.analysis.energy_models import SymmetryModel
×
397

398
        assert isinstance(trans.energy_model, SymmetryModel)
×
399

400
    def test_zero_spin_case(self):
1✔
401
        # ensure that zero spin case maintains sites and formula
402
        s = self.get_structure("Li2O")
×
403
        trans = MagOrderingTransformation({"Li+": 0.0}, order_parameter=0.5)
×
404
        alls = trans.apply_transformation(s)
×
405
        Li_site = alls.indices_from_symbol("Li")[0]
×
406
        # Ensure s does not have a spin property
407
        assert "spin" not in s.sites[Li_site].specie._properties
×
408
        # ensure sites are assigned a spin property in alls
409
        assert "spin" in alls.sites[Li_site].specie._properties
×
410
        assert alls.sites[Li_site].specie._properties["spin"] == 0
×
411

412
    def test_advanced_usage(self):
1✔
413
        # test spin on just one oxidation state
414
        magtypes = {"Fe2+": 5}
×
415
        trans = MagOrderingTransformation(magtypes)
×
416
        alls = trans.apply_transformation(self.Fe3O4_oxi)
×
417
        assert isinstance(alls, Structure)
×
418
        assert str(alls[0].specie) == "Fe2+,spin=5"
×
419
        assert str(alls[2].specie) == "Fe3+"
×
420

421
        # test multiple order parameters
422
        # this should only order on Fe3+ site, but assign spin to both
423
        magtypes = {"Fe2+": 5, "Fe3+": 5}
×
424
        order_parameters = [
×
425
            MagOrderParameterConstraint(1, species_constraints="Fe2+"),
426
            MagOrderParameterConstraint(0.5, species_constraints="Fe3+"),
427
        ]
428
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
429
        alls = trans.apply_transformation(self.Fe3O4_oxi)
×
430
        # using this 'sorted' syntax because exact order of sites in first
431
        # returned structure varies between machines: we just want to ensure
432
        # that the order parameter is accurate
433
        assert sorted(str(alls[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=5", "Fe2+,spin=5"])
×
434
        assert sorted(str(alls[idx].specie) for idx in range(2, 6)) == sorted(
×
435
            ["Fe3+,spin=5", "Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5"]
436
        )
437
        assert str(alls[0].specie) == "Fe2+,spin=5"
×
438

439
        # this should give same results as previously
440
        # but with opposite sign on Fe2+ site
441
        magtypes = {"Fe2+": -5, "Fe3+": 5}
×
442
        order_parameters = [
×
443
            MagOrderParameterConstraint(1, species_constraints="Fe2+"),
444
            MagOrderParameterConstraint(0.5, species_constraints="Fe3+"),
445
        ]
446
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
447
        alls = trans.apply_transformation(self.Fe3O4_oxi)
×
448
        assert sorted(str(alls[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=-5", "Fe2+,spin=-5"])
×
449
        assert sorted(str(alls[idx].specie) for idx in range(2, 6)) == sorted(
×
450
            ["Fe3+,spin=5", "Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5"]
451
        )
452

453
        # while this should order on both sites
454
        magtypes = {"Fe2+": 5, "Fe3+": 5}
×
455
        order_parameters = [
×
456
            MagOrderParameterConstraint(0.5, species_constraints="Fe2+"),
457
            MagOrderParameterConstraint(0.25, species_constraints="Fe3+"),
458
        ]
459
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
460
        alls = trans.apply_transformation(self.Fe3O4_oxi)
×
461
        assert sorted(str(alls[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=5", "Fe2+,spin=-5"])
×
462
        assert sorted(str(alls[idx].specie) for idx in range(2, 6)) == sorted(
×
463
            ["Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5", "Fe3+,spin=-5"]
464
        )
465

466
        # add coordination numbers to our test case
467
        # don't really care what these are for the test case
468
        cns = [6, 6, 6, 6, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0]
×
469
        self.Fe3O4.add_site_property("cn", cns)
×
470

471
        # this should give FM ordering on cn=4 sites, and AFM ordering on cn=6 sites
472
        magtypes = {"Fe": 5}
×
473
        order_parameters = [
×
474
            MagOrderParameterConstraint(
475
                0.5,
476
                species_constraints="Fe",
477
                site_constraint_name="cn",
478
                site_constraints=6,
479
            ),
480
            MagOrderParameterConstraint(
481
                1.0,
482
                species_constraints="Fe",
483
                site_constraint_name="cn",
484
                site_constraints=4,
485
            ),
486
        ]
487
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
488
        alls = trans.apply_transformation(self.Fe3O4)
×
489
        alls.sort(key=lambda x: x.properties["cn"], reverse=True)
×
490
        assert sorted(str(alls[idx].specie) for idx in range(0, 4)) == sorted(
×
491
            ["Fe,spin=-5", "Fe,spin=-5", "Fe,spin=5", "Fe,spin=5"]
492
        )
493
        assert sorted(str(alls[idx].specie) for idx in range(4, 6)) == sorted(["Fe,spin=5", "Fe,spin=5"])
×
494

495
        # now ordering on both sites, equivalent to order_parameter = 0.5
496
        magtypes = {"Fe2+": 5, "Fe3+": 5}
×
497
        order_parameters = [
×
498
            MagOrderParameterConstraint(0.5, species_constraints="Fe2+"),
499
            MagOrderParameterConstraint(0.5, species_constraints="Fe3+"),
500
        ]
501
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
502
        alls = trans.apply_transformation(self.Fe3O4_oxi, return_ranked_list=10)
×
503
        struct = alls[0]["structure"]
×
504
        assert sorted(str(struct[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=5", "Fe2+,spin=-5"])
×
505
        assert sorted(str(struct[idx].specie) for idx in range(2, 6)) == sorted(
×
506
            ["Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5", "Fe3+,spin=5"]
507
        )
508
        assert len(alls) == 4
×
509

510
        # now mixed orderings where neither are equal or 1
511
        magtypes = {"Fe2+": 5, "Fe3+": 5}
×
512
        order_parameters = [
×
513
            MagOrderParameterConstraint(0.5, species_constraints="Fe2+"),
514
            MagOrderParameterConstraint(0.25, species_constraints="Fe3+"),
515
        ]
516
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
517
        alls = trans.apply_transformation(self.Fe3O4_oxi, return_ranked_list=100)
×
518
        struct = alls[0]["structure"]
×
519
        assert sorted(str(struct[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=5", "Fe2+,spin=-5"])
×
520
        assert sorted(str(struct[idx].specie) for idx in range(2, 6)) == sorted(
×
521
            ["Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5", "Fe3+,spin=-5"]
522
        )
523
        assert len(alls) == 2
×
524

525
        # now order on multiple species
526
        magtypes = {"Fe2+": 5, "Fe3+": 5}
×
527
        order_parameters = [
×
528
            MagOrderParameterConstraint(0.5, species_constraints=["Fe2+", "Fe3+"]),
529
        ]
530
        trans = MagOrderingTransformation(magtypes, order_parameter=order_parameters)
×
531
        alls = trans.apply_transformation(self.Fe3O4_oxi, return_ranked_list=10)
×
532
        struct = alls[0]["structure"]
×
533
        assert sorted(str(struct[idx].specie) for idx in range(0, 2)) == sorted(["Fe2+,spin=5", "Fe2+,spin=-5"])
×
534
        assert sorted(str(struct[idx].specie) for idx in range(2, 6)) == sorted(
×
535
            ["Fe3+,spin=5", "Fe3+,spin=-5", "Fe3+,spin=-5", "Fe3+,spin=5"]
536
        )
537
        assert len(alls) == 6
×
538

539

540
@unittest.skipIf(not enumlib_present, "enum_lib not present.")
1✔
541
class DopingTransformationTest(PymatgenTest):
1✔
542
    def setUp(self):
1✔
543
        warnings.simplefilter("ignore")
×
544

545
    def tearDown(self):
1✔
546
        warnings.simplefilter("default")
×
547

548
    def test_apply_transformation(self):
1✔
549
        structure = PymatgenTest.get_structure("LiFePO4")
×
550
        a = SpacegroupAnalyzer(structure, 0.1)
×
551
        structure = a.get_refined_structure()
×
552
        t = DopingTransformation("Ca2+", min_length=10)
×
553
        ss = t.apply_transformation(structure, 100)
×
554
        assert len(ss) == 1
×
555

556
        t = DopingTransformation("Al3+", min_length=15, ionic_radius_tol=0.1)
×
557
        ss = t.apply_transformation(structure, 100)
×
558
        assert len(ss) == 0
×
559

560
        # Aliovalent doping with vacancies
561
        for dopant, nstructures in [("Al3+", 2), ("N3-", 235), ("Cl-", 8)]:
×
562
            t = DopingTransformation(dopant, min_length=4, alio_tol=1, max_structures_per_enum=1000)
×
563
            ss = t.apply_transformation(structure, 1000)
×
564
            assert len(ss) == nstructures
×
565
            for d in ss:
×
566
                assert d["structure"].charge == 0
×
567

568
        # Aliovalent doping with codopant
569
        for dopant, nstructures in [("Al3+", 3), ("N3-", 37), ("Cl-", 37)]:
×
570
            t = DopingTransformation(
×
571
                dopant,
572
                min_length=4,
573
                alio_tol=1,
574
                codopant=True,
575
                max_structures_per_enum=1000,
576
            )
577
            ss = t.apply_transformation(structure, 1000)
×
578
            assert len(ss) == nstructures
×
579
            for d in ss:
×
580
                assert d["structure"].charge == 0
×
581

582
        # Make sure compensation is done with lowest oxi state
583
        structure = PymatgenTest.get_structure("SrTiO3")
×
584
        t = DopingTransformation(
×
585
            "Nb5+",
586
            min_length=5,
587
            alio_tol=1,
588
            max_structures_per_enum=1000,
589
            allowed_doping_species=["Ti4+"],
590
        )
591
        ss = t.apply_transformation(structure, 1000)
×
592
        assert len(ss) == 3
×
593
        for d in ss:
×
594
            assert d["structure"].formula == "Sr7 Ti6 Nb2 O24"
×
595

596
    def test_as_from_dict(self):
1✔
597
        trans = DopingTransformation("Al3+", min_length=5, alio_tol=1, codopant=False, max_structures_per_enum=1)
×
598
        d = trans.as_dict()
×
599
        # Check json encodability
600
        _ = json.dumps(d)
×
601
        trans = DopingTransformation.from_dict(d)
×
602
        assert str(trans.dopant) == "Al3+"
×
603
        assert trans.max_structures_per_enum == 1
×
604

605
    def test_find_codopant(self):
1✔
606
        assert _find_codopant(Species("Fe", 2), 1) == Species("Cu", 1)
×
607
        assert _find_codopant(Species("Fe", 2), 3) == Species("In", 3)
×
608

609

610
class SlabTransformationTest(PymatgenTest):
1✔
611
    def test_apply_transformation(self):
1✔
612
        s = self.get_structure("LiFePO4")
1✔
613
        trans = SlabTransformation([0, 0, 1], 10, 10, shift=0.25)
1✔
614
        gen = SlabGenerator(s, [0, 0, 1], 10, 10)
1✔
615
        slab_from_gen = gen.get_slab(0.25)
1✔
616
        slab_from_trans = trans.apply_transformation(s)
1✔
617
        self.assertArrayAlmostEqual(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix)
1✔
618
        self.assertArrayAlmostEqual(slab_from_gen.cart_coords, slab_from_trans.cart_coords)
1✔
619

620
        fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]])
1✔
621
        trans = SlabTransformation([1, 1, 1], 10, 10)
1✔
622
        slab_from_trans = trans.apply_transformation(fcc)
1✔
623
        gen = SlabGenerator(fcc, [1, 1, 1], 10, 10)
1✔
624
        slab_from_gen = gen.get_slab()
1✔
625
        self.assertArrayAlmostEqual(slab_from_gen.lattice.matrix, slab_from_trans.lattice.matrix)
1✔
626
        self.assertArrayAlmostEqual(slab_from_gen.cart_coords, slab_from_trans.cart_coords)
1✔
627

628

629
class GrainBoundaryTransformationTest(PymatgenTest):
1✔
630
    def test_apply_transformation(self):
1✔
631
        with warnings.catch_warnings():
1✔
632
            warnings.simplefilter("ignore")
1✔
633
            Al_bulk = Structure.from_spacegroup("Fm-3m", Lattice.cubic(2.8575585), ["Al"], [[0, 0, 0]])
1✔
634
            gb_gen_params_s5 = {
1✔
635
                "rotation_axis": [1, 0, 0],
636
                "rotation_angle": 53.13010235415599,
637
                "expand_times": 3,
638
                "vacuum_thickness": 0.0,
639
                "normal": True,
640
                "plane": [0, -1, -3],
641
                "rm_ratio": 0.6,
642
            }
643
            gbg = GrainBoundaryGenerator(Al_bulk)
1✔
644
            gb_from_generator = gbg.gb_from_parameters(**gb_gen_params_s5)
1✔
645
            gbt_s5 = GrainBoundaryTransformation(**gb_gen_params_s5)
1✔
646
            gb_from_trans = gbt_s5.apply_transformation(Al_bulk)
1✔
647
            self.assertArrayAlmostEqual(gb_from_generator.lattice.matrix, gb_from_trans.lattice.matrix)
1✔
648
            self.assertArrayAlmostEqual(gb_from_generator.cart_coords, gb_from_trans.cart_coords)
1✔
649

650

651
class DisorderedOrderedTransformationTest(PymatgenTest):
1✔
652
    def test_apply_transformation(self):
1✔
653
        # nonsensical example just for testing purposes
654
        struct = self.get_structure("BaNiO3")
1✔
655

656
        trans = DisorderOrderedTransformation()
1✔
657
        output = trans.apply_transformation(struct)
1✔
658

659
        assert not output.is_ordered
1✔
660
        assert output[-1].species.as_dict() == {"Ni": 0.5, "Ba": 0.5}
1✔
661

662

663
@unittest.skipIf(not mcsqs_cmd, "mcsqs not present.")
1✔
664
class SQSTransformationTest(PymatgenTest):
1✔
665
    def test_apply_transformation(self):
1✔
666
        pztstructs = loadfn(os.path.join(PymatgenTest.TEST_FILES_DIR, "mcsqs/pztstructs.json"))
×
667
        trans = SQSTransformation(scaling=[2, 1, 1], search_time=0.01, instances=1, wd=0)
×
668
        # nonsensical example just for testing purposes
669
        struct = self.get_structure("Pb2TiZrO6").copy()
×
670
        struct.replace_species({"Ti": {"Ti": 0.5, "Zr": 0.5}, "Zr": {"Ti": 0.5, "Zr": 0.5}})
×
671
        struc_out = trans.apply_transformation(struct)
×
672
        matches = [struc_out.matches(s) for s in pztstructs]
×
673
        assert True in matches
×
674

675
    def test_return_ranked_list(self):
1✔
676
        # list of structures
677
        pztstructs2 = loadfn(os.path.join(PymatgenTest.TEST_FILES_DIR, "mcsqs/pztstructs2.json"))
×
678
        trans = SQSTransformation(scaling=2, search_time=0.01, instances=8, wd=0)
×
679
        struct = self.get_structure("Pb2TiZrO6").copy()
×
680
        struct.replace_species({"Ti": {"Ti": 0.5, "Zr": 0.5}, "Zr": {"Ti": 0.5, "Zr": 0.5}})
×
681
        ranked_list_out = trans.apply_transformation(struct, return_ranked_list=True)
×
682
        matches = [ranked_list_out[0]["structure"].matches(s) for s in pztstructs2]
×
683
        assert True in matches
×
684

685
    def test_spin(self):
1✔
686
        trans = SQSTransformation(scaling=[2, 1, 1], search_time=0.01, instances=1, wd=0)
×
687

688
        # nonsensical example just for testing purposes
689
        struct = self.get_structure("Pb2TiZrO6").copy()
×
690
        struct.replace_species({"Ti": {"Ti,spin=5": 0.5, "Ti,spin=-5": 0.5}})
×
691

692
        struc_out = trans.apply_transformation(struct)
×
693
        struc_out_specie_strings = [site.species_string for site in struc_out]
×
694
        assert "Ti,spin=-5" in struc_out_specie_strings
×
695
        assert "Ti,spin=5" in struc_out_specie_strings
×
696

697

698
class CubicSupercellTransformationTest(PymatgenTest):
1✔
699
    def test_apply_transformation(self):
1✔
700
        structure = self.get_structure("TlBiSe2")
1✔
701
        min_atoms = 100
1✔
702
        max_atoms = 1000
1✔
703

704
        # Test the transformation without constraining trans_mat to be diagonal
705
        supercell_generator = CubicSupercellTransformation(min_atoms=min_atoms, max_atoms=max_atoms, min_length=13.0)
1✔
706
        superstructure = supercell_generator.apply_transformation(structure)
1✔
707

708
        num_atoms = superstructure.num_sites
1✔
709
        assert num_atoms >= min_atoms
1✔
710
        assert num_atoms <= max_atoms
1✔
711
        self.assertArrayAlmostEqual(
1✔
712
            superstructure.lattice.matrix[0],
713
            [1.49656087e01, -1.11448000e-03, 9.04924836e00],
714
        )
715
        self.assertArrayAlmostEqual(superstructure.lattice.matrix[1], [-0.95005506, 14.95766342, 10.01819773])
1✔
716
        self.assertArrayAlmostEqual(
1✔
717
            superstructure.lattice.matrix[2],
718
            [3.69130000e-02, 4.09320200e-02, 5.90830153e01],
719
        )
720
        assert superstructure.num_sites == 448
1✔
721
        self.assertArrayEqual(
1✔
722
            supercell_generator.transformation_matrix,
723
            np.array([[4, 0, 0], [1, 4, -4], [0, 0, 1]]),
724
        )
725

726
        # Test the diagonal transformation
727
        structure2 = self.get_structure("Si")
1✔
728
        sga = SpacegroupAnalyzer(structure2)
1✔
729
        structure2 = sga.get_primitive_standard_structure()
1✔
730
        diagonal_supercell_generator = CubicSupercellTransformation(
1✔
731
            min_atoms=min_atoms,
732
            max_atoms=max_atoms,
733
            min_length=13.0,
734
            force_diagonal=True,
735
        )
736
        _ = diagonal_supercell_generator.apply_transformation(structure2)
1✔
737
        self.assertArrayEqual(diagonal_supercell_generator.transformation_matrix, np.eye(3) * 4)
1✔
738

739
        # test force_90_degrees
740
        structure2 = self.get_structure("Si")
1✔
741
        sga = SpacegroupAnalyzer(structure2)
1✔
742
        structure2 = sga.get_primitive_standard_structure()
1✔
743
        diagonal_supercell_generator = CubicSupercellTransformation(
1✔
744
            min_atoms=min_atoms,
745
            max_atoms=max_atoms,
746
            min_length=13.0,
747
            force_90_degrees=True,
748
        )
749
        transformed_structure = diagonal_supercell_generator.apply_transformation(structure2)
1✔
750
        self.assertArrayAlmostEqual(list(transformed_structure.lattice.angles), [90.0, 90.0, 90.0])
1✔
751

752
        structure = self.get_structure("BaNiO3")
1✔
753
        min_atoms = 100
1✔
754
        max_atoms = 1000
1✔
755

756
        # Test the transformation without constraining trans_mat to be diagonal
757
        supercell_generator = CubicSupercellTransformation(
1✔
758
            min_atoms=min_atoms, max_atoms=max_atoms, min_length=10.0, force_90_degrees=True
759
        )
760
        transformed_structure = supercell_generator.apply_transformation(structure)
1✔
761
        self.assertArrayAlmostEqual(list(transformed_structure.lattice.angles), [90.0, 90.0, 90.0])
1✔
762

763

764
class AddAdsorbateTransformationTest(PymatgenTest):
1✔
765
    def test_apply_transformation(self):
1✔
766
        co = Molecule(["C", "O"], [[0, 0, 0], [0, 0, 1.23]])
1✔
767
        trans = AddAdsorbateTransformation(co)
1✔
768
        pt = Structure(Lattice.cubic(5), ["Pt"], [[0, 0, 0]])  # fictitious
1✔
769
        slab = SlabTransformation([0, 0, 1], 20, 10).apply_transformation(pt)
1✔
770
        out = trans.apply_transformation(slab)
1✔
771

772
        assert out.composition.reduced_formula == "Pt4CO"
1✔
773

774

775
class SubstituteSurfaceSiteTransformationTest(PymatgenTest):
1✔
776
    def test_apply_transformation(self):
1✔
777
        trans = SubstituteSurfaceSiteTransformation("Au")
1✔
778
        pt = Structure(Lattice.cubic(5), ["Pt"], [[0, 0, 0]])  # fictitious
1✔
779
        slab = SlabTransformation([0, 0, 1], 20, 10).apply_transformation(pt)
1✔
780
        out = trans.apply_transformation(slab)
1✔
781

782
        assert out.composition.reduced_formula == "Pt3Au"
1✔
783

784

785
@unittest.skipIf(not hiphive, "hiphive not present. Skipping...")
1✔
786
class MonteCarloRattleTransformationTest(PymatgenTest):
1✔
787
    def test_apply_transformation(self):
1✔
788
        s = self.get_structure("Si")
×
789
        mcrt = MonteCarloRattleTransformation(0.01, 2, seed=1)
×
790
        s_trans = mcrt.apply_transformation(s)
×
791

792
        assert not np.allclose(s.cart_coords, s_trans.cart_coords, atol=0.01)
×
793
        assert np.allclose(s.cart_coords, s_trans.cart_coords, atol=1)
×
794

795
        # test using same seed gives same coords
796
        mcrt = MonteCarloRattleTransformation(0.01, 2, seed=1)
×
797
        s_trans2 = mcrt.apply_transformation(s)
×
798
        assert np.allclose(s_trans.cart_coords, s_trans2.cart_coords)
×
799

800

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