• 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

99.78
/pymatgen/analysis/tests/test_structure_matcher.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4

5
from __future__ import annotations
1✔
6

7
import json
1✔
8
import os
1✔
9
import unittest
1✔
10

11
import numpy as np
1✔
12
import pytest
1✔
13
from monty.json import MontyDecoder
1✔
14
from pytest import approx
1✔
15

16
from pymatgen.analysis.structure_matcher import (
1✔
17
    ElementComparator,
18
    FrameworkComparator,
19
    OccupancyComparator,
20
    OrderDisorderElementComparator,
21
    StructureMatcher,
22
)
23
from pymatgen.core.lattice import Lattice
1✔
24
from pymatgen.core.operations import SymmOp
1✔
25
from pymatgen.core.periodic_table import Element
1✔
26
from pymatgen.core.structure import Structure
1✔
27
from pymatgen.util.coord import find_in_coord_list_pbc
1✔
28
from pymatgen.util.testing import PymatgenTest
1✔
29

30

31
class StructureMatcherTest(PymatgenTest):
1✔
32
    _multiprocess_shared_ = True
1✔
33

34
    def setUp(self):
1✔
35
        with open(os.path.join(PymatgenTest.TEST_FILES_DIR, "TiO2_entries.json")) as fp:
1✔
36
            entries = json.load(fp, cls=MontyDecoder)
1✔
37
        self.struct_list = [e.structure for e in entries]
1✔
38
        self.oxi_structs = [
1✔
39
            self.get_structure("Li2O"),
40
            Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR.Li2O")),
41
        ]
42

43
    def test_ignore_species(self):
1✔
44
        s1 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "LiFePO4.cif"))
1✔
45
        s2 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "POSCAR"))
1✔
46
        m = StructureMatcher(ignored_species=["Li"], primitive_cell=False, attempt_supercell=True)
1✔
47
        assert m.fit(s1, s2)
1✔
48
        assert m.fit_anonymous(s1, s2)
1✔
49
        groups = m.group_structures([s1, s2])
1✔
50
        assert len(groups) == 1
1✔
51
        s2.make_supercell((2, 1, 1))
1✔
52
        ss1 = m.get_s2_like_s1(s2, s1, include_ignored_species=True)
1✔
53
        assert ss1.lattice.a == approx(20.820740000000001)
1✔
54
        assert ss1.composition.reduced_formula == "LiFePO4"
1✔
55

56
        assert {k.symbol: v.symbol for k, v in m.get_best_electronegativity_anonymous_mapping(s1, s2).items()} == {
1✔
57
            "Fe": "Fe",
58
            "P": "P",
59
            "O": "O",
60
        }
61

62
    def test_get_supercell_size(self):
1✔
63
        l = Lattice.cubic(1)
1✔
64
        l2 = Lattice.cubic(0.9)
1✔
65
        s1 = Structure(l, ["Mg", "Cu", "Ag", "Cu", "Ag"], [[0] * 3] * 5)
1✔
66
        s2 = Structure(l2, ["Cu", "Cu", "Ag"], [[0] * 3] * 3)
1✔
67

68
        sm = StructureMatcher(supercell_size="volume")
1✔
69
        assert sm._get_supercell_size(s1, s2) == (1, True)
1✔
70
        assert sm._get_supercell_size(s2, s1) == (1, True)
1✔
71

72
        sm = StructureMatcher(supercell_size="num_sites")
1✔
73
        assert sm._get_supercell_size(s1, s2) == (2, False)
1✔
74
        assert sm._get_supercell_size(s2, s1) == (2, True)
1✔
75

76
        sm = StructureMatcher(supercell_size="Ag")
1✔
77
        assert sm._get_supercell_size(s1, s2) == (2, False)
1✔
78
        assert sm._get_supercell_size(s2, s1) == (2, True)
1✔
79

80
        sm = StructureMatcher(supercell_size=["Ag", "Cu"])
1✔
81
        assert sm._get_supercell_size(s1, s2) == (1, True)
1✔
82
        assert sm._get_supercell_size(s2, s1) == (1, True)
1✔
83

84
        sm = StructureMatcher(supercell_size="wfieoh")
1✔
85
        with pytest.raises(ValueError):
1✔
86
            sm._get_supercell_size(s1, s2)
1✔
87

88
    def test_cmp_fstruct(self):
1✔
89
        sm = StructureMatcher()
1✔
90

91
        s1 = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
1✔
92
        s2 = np.array([[0.11, 0.22, 0.33]])
1✔
93
        frac_tol = np.array([0.02, 0.03, 0.04])
1✔
94
        mask = np.array([[False, False]])
1✔
95
        mask2 = np.array([[True, False]])
1✔
96

97
        with pytest.raises(ValueError):
1✔
98
            sm._cmp_fstruct(s2, s1, frac_tol, mask.T)
1✔
99
        with pytest.raises(ValueError):
1✔
100
            sm._cmp_fstruct(s1, s2, frac_tol, mask.T)
1✔
101

102
        assert sm._cmp_fstruct(s1, s2, frac_tol, mask)
1✔
103
        assert not sm._cmp_fstruct(s1, s2, frac_tol / 2, mask)
1✔
104
        assert not sm._cmp_fstruct(s1, s2, frac_tol, mask2)
1✔
105

106
    def test_cart_dists(self):
1✔
107
        sm = StructureMatcher()
1✔
108
        l = Lattice.orthorhombic(1, 2, 3)
1✔
109

110
        s1 = np.array([[0.13, 0.25, 0.37], [0.1, 0.2, 0.3]])
1✔
111
        s2 = np.array([[0.11, 0.22, 0.33]])
1✔
112
        s3 = np.array([[0.1, 0.2, 0.3], [0.11, 0.2, 0.3]])
1✔
113
        s4 = np.array([[0.1, 0.2, 0.3], [0.1, 0.6, 0.7]])
1✔
114
        mask = np.array([[False, False]])
1✔
115
        mask2 = np.array([[False, True]])
1✔
116
        mask3 = np.array([[False, False], [False, False]])
1✔
117
        mask4 = np.array([[False, True], [False, True]])
1✔
118

119
        n1 = (len(s1) / l.volume) ** (1 / 3)
1✔
120
        n2 = (len(s2) / l.volume) ** (1 / 3)
1✔
121

122
        with pytest.raises(ValueError):
1✔
123
            sm._cart_dists(s2, s1, l, mask.T, n2)
1✔
124
        with pytest.raises(ValueError):
1✔
125
            sm._cart_dists(s1, s2, l, mask.T, n1)
1✔
126

127
        d, ft, s = sm._cart_dists(s1, s2, l, mask, n1)
1✔
128
        assert np.allclose(d, [0])
1✔
129
        assert np.allclose(ft, [-0.01, -0.02, -0.03])
1✔
130
        assert np.allclose(s, [1])
1✔
131

132
        # check that masking best value works
133
        d, ft, s = sm._cart_dists(s1, s2, l, mask2, n1)
1✔
134
        assert np.allclose(d, [0])
1✔
135
        assert np.allclose(ft, [0.02, 0.03, 0.04])
1✔
136
        assert np.allclose(s, [0])
1✔
137

138
        # check that averaging of translation is done properly
139
        d, ft, s = sm._cart_dists(s1, s3, l, mask3, n1)
1✔
140
        assert np.allclose(d, [0.08093341] * 2)
1✔
141
        assert np.allclose(ft, [0.01, 0.025, 0.035])
1✔
142
        assert np.allclose(s, [1, 0])
1✔
143

144
        # check distances are large when mask allows no 'real' mapping
145
        d, ft, s = sm._cart_dists(s1, s4, l, mask4, n1)
1✔
146
        assert np.min(d) > 1e8
1✔
147
        assert np.min(ft) > 1e8
1✔
148

149
    def test_get_mask(self):
1✔
150
        sm = StructureMatcher(comparator=ElementComparator())
1✔
151
        l = Lattice.cubic(1)
1✔
152
        s1 = Structure(l, ["Mg", "Cu", "Ag", "Cu"], [[0] * 3] * 4)
1✔
153
        s2 = Structure(l, ["Cu", "Cu", "Ag"], [[0] * 3] * 3)
1✔
154

155
        result = [
1✔
156
            [True, False, True, False],
157
            [True, False, True, False],
158
            [True, True, False, True],
159
        ]
160
        m, inds, i = sm._get_mask(s1, s2, 1, True)
1✔
161
        assert np.all(m == result)
1✔
162
        assert i == 2
1✔
163
        assert inds == [2]
1✔
164

165
        # test supercell with match
166
        result = [
1✔
167
            [1, 1, 0, 0, 1, 1, 0, 0],
168
            [1, 1, 0, 0, 1, 1, 0, 0],
169
            [1, 1, 1, 1, 0, 0, 1, 1],
170
        ]
171
        m, inds, i = sm._get_mask(s1, s2, 2, True)
1✔
172
        assert np.all(m == result)
1✔
173
        assert i == 2
1✔
174
        assert np.allclose(inds, np.array([4]))
1✔
175

176
        # test supercell without match
177
        result = [
1✔
178
            [1, 1, 1, 1, 1, 1],
179
            [0, 0, 0, 0, 1, 1],
180
            [1, 1, 1, 1, 0, 0],
181
            [0, 0, 0, 0, 1, 1],
182
        ]
183
        m, inds, i = sm._get_mask(s2, s1, 2, True)
1✔
184
        assert np.all(m == result)
1✔
185
        assert i == 0
1✔
186
        assert np.allclose(inds, np.array([]))
1✔
187

188
        # test s2_supercell
189
        result = [
1✔
190
            [1, 1, 1],
191
            [1, 1, 1],
192
            [0, 0, 1],
193
            [0, 0, 1],
194
            [1, 1, 0],
195
            [1, 1, 0],
196
            [0, 0, 1],
197
            [0, 0, 1],
198
        ]
199
        m, inds, i = sm._get_mask(s2, s1, 2, False)
1✔
200
        assert np.all(m == result)
1✔
201
        assert i == 0
1✔
202
        assert np.allclose(inds, np.array([]))
1✔
203

204
        # test for multiple translation indices
205
        s1 = Structure(l, ["Cu", "Ag", "Cu", "Ag", "Ag"], [[0] * 3] * 5)
1✔
206
        s2 = Structure(l, ["Ag", "Cu", "Ag"], [[0] * 3] * 3)
1✔
207
        result = [[1, 0, 1, 0, 0], [0, 1, 0, 1, 1], [1, 0, 1, 0, 0]]
1✔
208
        m, inds, i = sm._get_mask(s1, s2, 1, True)
1✔
209

210
        assert np.all(m == result)
1✔
211
        assert i == 1
1✔
212
        assert np.allclose(inds, [0, 2])
1✔
213

214
    def test_get_supercells(self):
1✔
215
        sm = StructureMatcher(comparator=ElementComparator())
1✔
216
        l = Lattice.cubic(1)
1✔
217
        l2 = Lattice.cubic(0.5)
1✔
218
        s1 = Structure(l, ["Mg", "Cu", "Ag", "Cu"], [[0] * 3] * 4)
1✔
219
        s2 = Structure(l2, ["Cu", "Cu", "Ag"], [[0] * 3] * 3)
1✔
220
        scs = list(sm._get_supercells(s1, s2, 8, False))
1✔
221
        for x in scs:
1✔
222
            assert abs(np.linalg.det(x[3])) == approx(8)
1✔
223
            assert len(x[0]) == 4
1✔
224
            assert len(x[1]) == 24
1✔
225
        assert len(scs) == 48
1✔
226

227
        scs = list(sm._get_supercells(s2, s1, 8, True))
1✔
228
        for x in scs:
1✔
229
            assert abs(np.linalg.det(x[3])) == approx(8)
1✔
230
            assert len(x[0]) == 24
1✔
231
            assert len(x[1]) == 4
1✔
232
        assert len(scs) == 48
1✔
233

234
    def test_fit(self):
1✔
235
        """
236
        Take two known matched structures
237
            1) Ensure match
238
            2) Ensure match after translation and rotations
239
            3) Ensure no-match after large site translation
240
            4) Ensure match after site shuffling
241
        """
242
        sm = StructureMatcher()
1✔
243

244
        assert sm.fit(self.struct_list[0], self.struct_list[1])
1✔
245

246
        # Test rotational/translational invariance
247
        op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 30, False, np.array([0.4, 0.7, 0.9]))
1✔
248
        self.struct_list[1].apply_operation(op)
1✔
249
        assert sm.fit(self.struct_list[0], self.struct_list[1])
1✔
250

251
        # Test failure under large atomic translation
252
        self.struct_list[1].translate_sites([0], [0.4, 0.4, 0.2], frac_coords=True)
1✔
253
        assert not sm.fit(self.struct_list[0], self.struct_list[1])
1✔
254

255
        self.struct_list[1].translate_sites([0], [-0.4, -0.4, -0.2], frac_coords=True)
1✔
256
        # random.shuffle(editor._sites)
257
        assert sm.fit(self.struct_list[0], self.struct_list[1])
1✔
258
        # Test FrameworkComporator
259
        sm2 = StructureMatcher(comparator=FrameworkComparator())
1✔
260
        lfp = self.get_structure("LiFePO4")
1✔
261
        nfp = self.get_structure("NaFePO4")
1✔
262
        assert sm2.fit(lfp, nfp)
1✔
263
        assert not sm.fit(lfp, nfp)
1✔
264

265
        # Test anonymous fit.
266
        assert sm.fit_anonymous(lfp, nfp) is True
1✔
267
        assert sm.get_rms_anonymous(lfp, nfp)[0] == approx(0.060895871160262717)
1✔
268

269
        # Test partial occupancies.
270
        s1 = Structure(
1✔
271
            Lattice.cubic(3),
272
            [{"Fe": 0.5}, {"Fe": 0.5}, {"Fe": 0.5}, {"Fe": 0.5}],
273
            [[0, 0, 0], [0.25, 0.25, 0.25], [0.5, 0.5, 0.5], [0.75, 0.75, 0.75]],
274
        )
275
        s2 = Structure(
1✔
276
            Lattice.cubic(3),
277
            [{"Fe": 0.25}, {"Fe": 0.5}, {"Fe": 0.5}, {"Fe": 0.75}],
278
            [[0, 0, 0], [0.25, 0.25, 0.25], [0.5, 0.5, 0.5], [0.75, 0.75, 0.75]],
279
        )
280
        assert not sm.fit(s1, s2)
1✔
281
        assert not sm.fit(s2, s1)
1✔
282
        s2 = Structure(
1✔
283
            Lattice.cubic(3),
284
            [{"Mn": 0.5}, {"Mn": 0.5}, {"Mn": 0.5}, {"Mn": 0.5}],
285
            [[0, 0, 0], [0.25, 0.25, 0.25], [0.5, 0.5, 0.5], [0.75, 0.75, 0.75]],
286
        )
287
        assert sm.fit_anonymous(s1, s2) is True
1✔
288

289
        assert sm.get_rms_anonymous(s1, s2)[0] == approx(0)
1✔
290

291
        # test symmetric
292
        sm_coarse = sm = StructureMatcher(
1✔
293
            comparator=ElementComparator(),
294
            ltol=0.6,
295
            stol=0.6,
296
            angle_tol=6,
297
        )
298

299
        s1 = Structure.from_file(PymatgenTest.TEST_FILES_DIR / "fit_symm_s1.vasp")
1✔
300
        s2 = Structure.from_file(PymatgenTest.TEST_FILES_DIR / "fit_symm_s2.vasp")
1✔
301
        assert sm_coarse.fit(s1, s2)
1✔
302
        assert sm_coarse.fit(s2, s1) is False
1✔
303
        assert sm_coarse.fit(s1, s2, symmetric=True) is False
1✔
304
        assert sm_coarse.fit(s2, s1, symmetric=True) is False
1✔
305

306
    def test_oxi(self):
1✔
307
        """Test oxidation state removal matching"""
308
        sm = StructureMatcher()
1✔
309
        assert not sm.fit(self.oxi_structs[0], self.oxi_structs[1])
1✔
310
        sm = StructureMatcher(comparator=ElementComparator())
1✔
311
        assert sm.fit(self.oxi_structs[0], self.oxi_structs[1])
1✔
312

313
    def test_primitive(self):
1✔
314
        """Test primitive cell reduction"""
315
        sm = StructureMatcher(primitive_cell=True)
1✔
316
        self.struct_list[1].make_supercell([[2, 0, 0], [0, 3, 0], [0, 0, 1]])
1✔
317
        assert sm.fit(self.struct_list[0], self.struct_list[1])
1✔
318

319
    def test_class(self):
1✔
320
        # Tests entire class as single working unit
321
        sm = StructureMatcher()
1✔
322
        # Test group_structures and find_indices
323
        out = sm.group_structures(self.struct_list)
1✔
324
        assert list(map(len, out)) == [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]
1✔
325
        assert sum(map(len, out)) == len(self.struct_list)
1✔
326
        for s in self.struct_list[::2]:
1✔
327
            s.replace_species({"Ti": "Zr", "O": "Ti"})
1✔
328
        out = sm.group_structures(self.struct_list, anonymous=True)
1✔
329
        assert list(map(len, out)) == [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]
1✔
330

331
    def test_mix(self):
1✔
332
        structures = [
1✔
333
            self.get_structure("Li2O"),
334
            self.get_structure("Li2O2"),
335
            self.get_structure("LiFePO4"),
336
        ]
337
        for fname in ["POSCAR.Li2O", "POSCAR.LiFePO4"]:
1✔
338
            structures.append(Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, fname)))
1✔
339
        sm = StructureMatcher(comparator=ElementComparator())
1✔
340
        groups = sm.group_structures(structures)
1✔
341
        for g in groups:
1✔
342
            formula = g[0].composition.reduced_formula
1✔
343
            if formula in ["Li2O", "LiFePO4"]:
1✔
344
                assert len(g) == 2
1✔
345
            else:
346
                assert len(g) == 1
1✔
347

348
    def test_left_handed_lattice(self):
1✔
349
        """Ensure Left handed lattices are accepted"""
350
        sm = StructureMatcher()
1✔
351
        s = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Li3GaPCO7.json"))
1✔
352
        assert sm.fit(s, s)
1✔
353

354
    def test_as_dict_and_from_dict(self):
1✔
355
        sm = StructureMatcher(
1✔
356
            ltol=0.1,
357
            stol=0.2,
358
            angle_tol=2,
359
            primitive_cell=False,
360
            scale=False,
361
            comparator=FrameworkComparator(),
362
        )
363
        d = sm.as_dict()
1✔
364
        sm2 = StructureMatcher.from_dict(d)
1✔
365
        assert sm2.as_dict() == d
1✔
366

367
    def test_no_scaling(self):
1✔
368
        sm = StructureMatcher(ltol=0.1, stol=0.1, angle_tol=2, scale=False, comparator=ElementComparator())
1✔
369
        assert sm.fit(self.struct_list[0], self.struct_list[1])
1✔
370

371
        assert sm.get_rms_dist(self.struct_list[0], self.struct_list[1])[0] < 0.0008
1✔
372

373
    def test_supercell_fit(self):
1✔
374
        sm = StructureMatcher(attempt_supercell=False)
1✔
375
        s1 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Al3F9.json"))
1✔
376
        s2 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Al3F9_distorted.json"))
1✔
377

378
        assert not sm.fit(s1, s2)
1✔
379

380
        sm = StructureMatcher(attempt_supercell=True)
1✔
381

382
        assert sm.fit(s1, s2)
1✔
383
        assert sm.fit(s2, s1)
1✔
384

385
    def test_get_lattices(self):
1✔
386
        sm = StructureMatcher(
1✔
387
            ltol=0.2,
388
            stol=0.3,
389
            angle_tol=5,
390
            primitive_cell=True,
391
            scale=True,
392
            attempt_supercell=False,
393
        )
394
        l1 = Lattice.from_parameters(1, 2.1, 1.9, 90, 89, 91)
1✔
395
        l2 = Lattice.from_parameters(1.1, 2, 2, 89, 91, 90)
1✔
396
        s1 = Structure(l1, [], [])
1✔
397
        s2 = Structure(l2, [], [])
1✔
398

399
        lattices = list(sm._get_lattices(s=s1, target_lattice=s2.lattice))
1✔
400
        assert len(lattices) == 16
1✔
401

402
        l3 = Lattice.from_parameters(1.1, 2, 20, 89, 91, 90)
1✔
403
        s3 = Structure(l3, [], [])
1✔
404

405
        lattices = list(sm._get_lattices(s=s1, target_lattice=s3.lattice))
1✔
406
        assert len(lattices) == 0
1✔
407

408
    def test_find_match1(self):
1✔
409
        sm = StructureMatcher(
1✔
410
            ltol=0.2,
411
            stol=0.3,
412
            angle_tol=5,
413
            primitive_cell=True,
414
            scale=True,
415
            attempt_supercell=False,
416
        )
417
        l = Lattice.orthorhombic(1, 2, 3)
1✔
418
        s1 = Structure(l, ["Si", "Si", "Ag"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
419
        s2 = Structure(l, ["Si", "Si", "Ag"], [[0, 0.1, 0], [0, 0.1, -0.95], [0.7, 0.5, 0.375]])
1✔
420

421
        s1, s2, fu, s1_supercell = sm._preprocess(s1, s2, False)
1✔
422
        match = sm._strict_match(s1, s2, fu, s1_supercell=True, use_rms=True, break_on_match=False)
1✔
423
        scale_matrix = match[2]
1✔
424
        s2.make_supercell(scale_matrix)
1✔
425
        fc = s2.frac_coords + match[3]
1✔
426
        fc -= np.round(fc)
1✔
427
        assert np.sum(fc) == approx(0.9)
1✔
428
        assert np.sum(fc[:, :2]) == approx(0.1)
1✔
429
        cart_dist = np.sum(match[1] * (l.volume / 3) ** (1 / 3))
1✔
430
        assert cart_dist == approx(0.15)
1✔
431

432
    def test_find_match2(self):
1✔
433
        sm = StructureMatcher(
1✔
434
            ltol=0.2,
435
            stol=0.3,
436
            angle_tol=5,
437
            primitive_cell=True,
438
            scale=True,
439
            attempt_supercell=False,
440
        )
441
        l = Lattice.orthorhombic(1, 2, 3)
1✔
442
        s1 = Structure(l, ["Si", "Si"], [[0, 0, 0.1], [0, 0, 0.2]])
1✔
443
        s2 = Structure(l, ["Si", "Si"], [[0, 0.1, 0], [0, 0.1, -0.95]])
1✔
444

445
        s1, s2, fu, s1_supercell = sm._preprocess(s1, s2, False)
1✔
446

447
        match = sm._strict_match(s1, s2, fu, s1_supercell=False, use_rms=True, break_on_match=False)
1✔
448
        scale_matrix = match[2]
1✔
449
        s2.make_supercell(scale_matrix)
1✔
450
        s2.translate_sites(range(len(s2)), match[3])
1✔
451

452
        assert np.sum(s2.frac_coords) % 1 == approx(0.3)
1✔
453
        assert np.sum(s2.frac_coords[:, :2]) % 1 == approx(0)
1✔
454

455
    def test_supercell_subsets(self):
1✔
456
        sm = StructureMatcher(
1✔
457
            ltol=0.2,
458
            stol=0.3,
459
            angle_tol=5,
460
            primitive_cell=False,
461
            scale=True,
462
            attempt_supercell=True,
463
            allow_subset=True,
464
            supercell_size="volume",
465
        )
466
        sm_no_s = StructureMatcher(
1✔
467
            ltol=0.2,
468
            stol=0.3,
469
            angle_tol=5,
470
            primitive_cell=False,
471
            scale=True,
472
            attempt_supercell=True,
473
            allow_subset=False,
474
            supercell_size="volume",
475
        )
476
        l = Lattice.orthorhombic(1, 2, 3)
1✔
477
        s1 = Structure(l, ["Ag", "Si", "Si"], [[0.7, 0.4, 0.5], [0, 0, 0.1], [0, 0, 0.2]])
1✔
478
        s1.make_supercell([2, 1, 1])
1✔
479
        s2 = Structure(l, ["Si", "Si", "Ag"], [[0, 0.1, -0.95], [0, 0.1, 0], [-0.7, 0.5, 0.375]])
1✔
480

481
        shuffle = [0, 2, 1, 3, 4, 5]
1✔
482
        s1 = Structure.from_sites([s1[i] for i in shuffle])
1✔
483

484
        # test when s1 is exact supercell of s2
485
        result = sm.get_s2_like_s1(s1, s2)
1✔
486
        for a, b in zip(s1, result):
1✔
487
            assert a.distance(b) < 0.08
1✔
488
            assert a.species == b.species
1✔
489

490
        assert sm.fit(s1, s2)
1✔
491
        assert sm.fit(s2, s1)
1✔
492
        assert sm_no_s.fit(s1, s2)
1✔
493
        assert sm_no_s.fit(s2, s1)
1✔
494

495
        rms = (0.048604032430991401, 0.059527539448807391)
1✔
496
        assert np.allclose(sm.get_rms_dist(s1, s2), rms)
1✔
497
        assert np.allclose(sm.get_rms_dist(s2, s1), rms)
1✔
498

499
        # test when the supercell is a subset of s2
500
        subset_supercell = s1.copy()
1✔
501
        del subset_supercell[0]
1✔
502
        result = sm.get_s2_like_s1(subset_supercell, s2)
1✔
503
        assert len(result) == 6
1✔
504
        for a, b in zip(subset_supercell, result):
1✔
505
            assert a.distance(b) < 0.08
1✔
506
            assert a.species == b.species
1✔
507

508
        assert sm.fit(subset_supercell, s2)
1✔
509
        assert sm.fit(s2, subset_supercell)
1✔
510
        assert not sm_no_s.fit(subset_supercell, s2)
1✔
511
        assert not sm_no_s.fit(s2, subset_supercell)
1✔
512

513
        rms = (0.053243049896333279, 0.059527539448807336)
1✔
514
        assert np.allclose(sm.get_rms_dist(subset_supercell, s2), rms)
1✔
515
        assert np.allclose(sm.get_rms_dist(s2, subset_supercell), rms)
1✔
516

517
        # test when s2 (once made a supercell) is a subset of s1
518
        s2_missing_site = s2.copy()
1✔
519
        del s2_missing_site[1]
1✔
520
        result = sm.get_s2_like_s1(s1, s2_missing_site)
1✔
521
        for a, b in zip((s1[i] for i in (0, 2, 4, 5)), result):
1✔
522
            assert a.distance(b) < 0.08
1✔
523
            assert a.species == b.species
1✔
524

525
        assert sm.fit(s1, s2_missing_site)
1✔
526
        assert sm.fit(s2_missing_site, s1)
1✔
527
        assert not sm_no_s.fit(s1, s2_missing_site)
1✔
528
        assert not sm_no_s.fit(s2_missing_site, s1)
1✔
529

530
        rms = (0.029763769724403633, 0.029763769724403987)
1✔
531
        assert np.allclose(sm.get_rms_dist(s1, s2_missing_site), rms)
1✔
532
        assert np.allclose(sm.get_rms_dist(s2_missing_site, s1), rms)
1✔
533

534
    def test_get_s2_large_s2(self):
1✔
535
        sm = StructureMatcher(
1✔
536
            ltol=0.2,
537
            stol=0.3,
538
            angle_tol=5,
539
            primitive_cell=False,
540
            scale=False,
541
            attempt_supercell=True,
542
            allow_subset=False,
543
            supercell_size="volume",
544
        )
545

546
        l = Lattice.orthorhombic(1, 2, 3)
1✔
547
        s1 = Structure(l, ["Ag", "Si", "Si"], [[0.7, 0.4, 0.5], [0, 0, 0.1], [0, 0, 0.2]])
1✔
548

549
        l2 = Lattice.orthorhombic(1.01, 2.01, 3.01)
1✔
550
        s2 = Structure(l2, ["Si", "Si", "Ag"], [[0, 0.1, -0.95], [0, 0.1, 0], [-0.7, 0.5, 0.375]])
1✔
551
        s2.make_supercell([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
1✔
552

553
        result = sm.get_s2_like_s1(s1, s2)
1✔
554

555
        for x, y in zip(s1, result):
1✔
556
            assert x.distance(y) < 0.08
1✔
557

558
    def test_get_mapping(self):
1✔
559
        sm = StructureMatcher(
1✔
560
            ltol=0.2,
561
            stol=0.3,
562
            angle_tol=5,
563
            primitive_cell=False,
564
            scale=True,
565
            attempt_supercell=False,
566
            allow_subset=True,
567
        )
568
        l = Lattice.orthorhombic(1, 2, 3)
1✔
569
        s1 = Structure(l, ["Ag", "Si", "Si"], [[0.7, 0.4, 0.5], [0, 0, 0.1], [0, 0, 0.2]])
1✔
570
        s1.make_supercell([2, 1, 1])
1✔
571
        s2 = Structure(l, ["Si", "Si", "Ag"], [[0, 0.1, -0.95], [0, 0.1, 0], [-0.7, 0.5, 0.375]])
1✔
572

573
        shuffle = [2, 0, 1, 3, 5, 4]
1✔
574
        s1 = Structure.from_sites([s1[i] for i in shuffle])
1✔
575
        # test the mapping
576
        s2.make_supercell([2, 1, 1])
1✔
577
        # equal sizes
578
        for i, x in enumerate(sm.get_mapping(s1, s2)):
1✔
579
            assert s1[x].species == s2[i].species
1✔
580

581
        del s1[0]
1✔
582
        # s1 is subset of s2
583
        for i, x in enumerate(sm.get_mapping(s2, s1)):
1✔
584
            assert s1[i].species == s2[x].species
1✔
585
        # s2 is smaller than s1
586
        del s2[0]
1✔
587
        del s2[1]
1✔
588
        with pytest.raises(ValueError):
1✔
589
            sm.get_mapping(s2, s1)
1✔
590

591
    def test_get_supercell_matrix(self):
1✔
592
        sm = StructureMatcher(
1✔
593
            ltol=0.1,
594
            stol=0.3,
595
            angle_tol=2,
596
            primitive_cell=False,
597
            scale=True,
598
            attempt_supercell=True,
599
        )
600

601
        l = Lattice.orthorhombic(1, 2, 3)
1✔
602

603
        s1 = Structure(l, ["Si", "Si", "Ag"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
604
        s1.make_supercell([2, 1, 1])
1✔
605
        s2 = Structure(l, ["Si", "Si", "Ag"], [[0, 0.1, 0], [0, 0.1, -0.95], [-0.7, 0.5, 0.375]])
1✔
606
        result = sm.get_supercell_matrix(s1, s2)
1✔
607
        assert (result == [[-2, 0, 0], [0, 1, 0], [0, 0, 1]]).all()
1✔
608

609
        s1 = Structure(l, ["Si", "Si", "Ag"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
610
        s1.make_supercell([[1, -1, 0], [0, 0, -1], [0, 1, 0]])
1✔
611

612
        s2 = Structure(l, ["Si", "Si", "Ag"], [[0, 0.1, 0], [0, 0.1, -0.95], [-0.7, 0.5, 0.375]])
1✔
613
        result = sm.get_supercell_matrix(s1, s2)
1✔
614
        assert (result == [[-1, -1, 0], [0, 0, -1], [0, 1, 0]]).all()
1✔
615

616
        # test when the supercell is a subset
617
        sm = StructureMatcher(
1✔
618
            ltol=0.1,
619
            stol=0.3,
620
            angle_tol=2,
621
            primitive_cell=False,
622
            scale=True,
623
            attempt_supercell=True,
624
            allow_subset=True,
625
        )
626
        del s1[0]
1✔
627
        result = sm.get_supercell_matrix(s1, s2)
1✔
628
        assert (result == [[-1, -1, 0], [0, 0, -1], [0, 1, 0]]).all()
1✔
629

630
    def test_subset(self):
1✔
631
        sm = StructureMatcher(
1✔
632
            ltol=0.2,
633
            stol=0.3,
634
            angle_tol=5,
635
            primitive_cell=False,
636
            scale=True,
637
            attempt_supercell=False,
638
            allow_subset=True,
639
        )
640
        l = Lattice.orthorhombic(10, 20, 30)
1✔
641
        s1 = Structure(l, ["Si", "Si", "Ag"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
642
        s2 = Structure(l, ["Si", "Ag"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
1✔
643
        result = sm.get_s2_like_s1(s1, s2)
1✔
644

645
        assert len(find_in_coord_list_pbc(result.frac_coords, [0, 0, 0.1])) == 1
1✔
646
        assert len(find_in_coord_list_pbc(result.frac_coords, [0.7, 0.4, 0.5])) == 1
1✔
647

648
        # test with fewer species in s2
649
        s1 = Structure(l, ["Si", "Ag", "Si"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
650
        s2 = Structure(l, ["Si", "Si"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
1✔
651
        result = sm.get_s2_like_s1(s1, s2)
1✔
652
        mindists = np.min(s1.lattice.get_all_distances(s1.frac_coords, result.frac_coords), axis=0)
1✔
653
        assert np.max(mindists) < 1e-6
1✔
654

655
        assert len(find_in_coord_list_pbc(result.frac_coords, [0, 0, 0.1])) == 1
1✔
656
        assert len(find_in_coord_list_pbc(result.frac_coords, [0.7, 0.4, 0.5])) == 1
1✔
657

658
        # test with not enough sites in s1
659
        # test with fewer species in s2
660
        s1 = Structure(l, ["Si", "Ag", "Cl"], [[0, 0, 0.1], [0, 0, 0.2], [0.7, 0.4, 0.5]])
1✔
661
        s2 = Structure(l, ["Si", "Si"], [[0, 0.1, 0], [-0.7, 0.5, 0.4]])
1✔
662
        assert sm.get_s2_like_s1(s1, s2) is None
1✔
663

664
    def test_out_of_cell_s2_like_s1(self):
1✔
665
        l = Lattice.cubic(5)
1✔
666
        s1 = Structure(l, ["Si", "Ag", "Si"], [[0, 0, -0.02], [0, 0, 0.001], [0.7, 0.4, 0.5]])
1✔
667
        s2 = Structure(l, ["Si", "Ag", "Si"], [[0, 0, 0.98], [0, 0, 0.99], [0.7, 0.4, 0.5]])
1✔
668
        new_s2 = StructureMatcher(primitive_cell=False).get_s2_like_s1(s1, s2)
1✔
669
        dists = np.sum((s1.cart_coords - new_s2.cart_coords) ** 2, axis=-1) ** 0.5
1✔
670
        assert np.max(dists) < 0.1
1✔
671

672
    def test_disordered_primitive_to_ordered_supercell(self):
1✔
673
        sm_atoms = StructureMatcher(
1✔
674
            ltol=0.2,
675
            stol=0.3,
676
            angle_tol=5,
677
            primitive_cell=False,
678
            scale=True,
679
            attempt_supercell=True,
680
            allow_subset=True,
681
            supercell_size="num_atoms",
682
            comparator=OrderDisorderElementComparator(),
683
        )
684
        sm_sites = StructureMatcher(
1✔
685
            ltol=0.2,
686
            stol=0.3,
687
            angle_tol=5,
688
            primitive_cell=False,
689
            scale=True,
690
            attempt_supercell=True,
691
            allow_subset=True,
692
            supercell_size="num_sites",
693
            comparator=OrderDisorderElementComparator(),
694
        )
695
        lp = Lattice.orthorhombic(10, 20, 30)
1✔
696
        pcoords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
697
        ls = Lattice.orthorhombic(20, 20, 30)
1✔
698
        scoords = [[0, 0, 0], [0.75, 0.5, 0.5]]
1✔
699
        prim = Structure(lp, [{"Na": 0.5}, {"Cl": 0.5}], pcoords)
1✔
700
        supercell = Structure(ls, ["Na", "Cl"], scoords)
1✔
701
        supercell.make_supercell([[-1, 1, 0], [0, 1, 1], [1, 0, 0]])
1✔
702

703
        assert not sm_sites.fit(prim, supercell)
1✔
704
        assert sm_atoms.fit(prim, supercell)
1✔
705

706
        with pytest.raises(ValueError):
1✔
707
            sm_atoms.get_s2_like_s1(prim, supercell)
1✔
708
        assert len(sm_atoms.get_s2_like_s1(supercell, prim)) == 4
1✔
709

710
    def test_ordered_primitive_to_disordered_supercell(self):
1✔
711
        sm_atoms = StructureMatcher(
1✔
712
            ltol=0.2,
713
            stol=0.3,
714
            angle_tol=5,
715
            primitive_cell=False,
716
            scale=True,
717
            attempt_supercell=True,
718
            allow_subset=True,
719
            supercell_size="num_atoms",
720
            comparator=OrderDisorderElementComparator(),
721
        )
722
        sm_sites = StructureMatcher(
1✔
723
            ltol=0.2,
724
            stol=0.3,
725
            angle_tol=5,
726
            primitive_cell=False,
727
            scale=True,
728
            attempt_supercell=True,
729
            allow_subset=True,
730
            supercell_size="num_sites",
731
            comparator=OrderDisorderElementComparator(),
732
        )
733
        lp = Lattice.orthorhombic(10, 20, 30)
1✔
734
        pcoords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
735
        ls = Lattice.orthorhombic(20, 20, 30)
1✔
736
        scoords = [[0, 0, 0], [0.5, 0, 0], [0.25, 0.5, 0.5], [0.75, 0.5, 0.5]]
1✔
737
        s1 = Structure(lp, ["Na", "Cl"], pcoords)
1✔
738
        s2 = Structure(ls, [{"Na": 0.5}, {"Na": 0.5}, {"Cl": 0.5}, {"Cl": 0.5}], scoords)
1✔
739

740
        assert sm_sites.fit(s1, s2)
1✔
741
        assert not sm_atoms.fit(s1, s2)
1✔
742

743
    def test_disordered_to_disordered(self):
1✔
744
        sm_atoms = StructureMatcher(
1✔
745
            ltol=0.2,
746
            stol=0.3,
747
            angle_tol=5,
748
            primitive_cell=False,
749
            scale=True,
750
            attempt_supercell=True,
751
            allow_subset=False,
752
            comparator=OrderDisorderElementComparator(),
753
        )
754
        lp = Lattice.orthorhombic(10, 20, 30)
1✔
755
        coords = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]
1✔
756
        s1 = Structure(lp, [{"Na": 0.5, "Cl": 0.5}, {"Na": 0.5, "Cl": 0.5}], coords)
1✔
757
        s2 = Structure(lp, [{"Na": 0.5, "Cl": 0.5}, {"Na": 0.5, "Br": 0.5}], coords)
1✔
758

759
        assert not sm_atoms.fit(s1, s2)
1✔
760

761
    def test_occupancy_comparator(self):
1✔
762
        lp = Lattice.orthorhombic(10, 20, 30)
1✔
763
        pcoords = [[0, 0, 0], [0.5, 0.5, 0.5]]
1✔
764
        s1 = Structure(lp, [{"Na": 0.6, "K": 0.4}, "Cl"], pcoords)
1✔
765
        s2 = Structure(lp, [{"Xa": 0.4, "Xb": 0.6}, "Cl"], pcoords)
1✔
766
        s3 = Structure(lp, [{"Xa": 0.5, "Xb": 0.5}, "Cl"], pcoords)
1✔
767

768
        sm_sites = StructureMatcher(
1✔
769
            ltol=0.2,
770
            stol=0.3,
771
            angle_tol=5,
772
            primitive_cell=False,
773
            scale=True,
774
            attempt_supercell=True,
775
            allow_subset=True,
776
            supercell_size="num_sites",
777
            comparator=OccupancyComparator(),
778
        )
779

780
        assert sm_sites.fit(s1, s2)
1✔
781
        assert not sm_sites.fit(s1, s3)
1✔
782

783
    def test_electronegativity(self):
1✔
784
        sm = StructureMatcher(ltol=0.2, stol=0.3, angle_tol=5)
1✔
785

786
        s1 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Na2Fe2PAsO4S4.json"))
1✔
787
        s2 = Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "Na2Fe2PNO4Se4.json"))
1✔
788
        assert sm.get_best_electronegativity_anonymous_mapping(s1, s2) == {
1✔
789
            Element("S"): Element("Se"),
790
            Element("As"): Element("N"),
791
            Element("Fe"): Element("Fe"),
792
            Element("Na"): Element("Na"),
793
            Element("P"): Element("P"),
794
            Element("O"): Element("O"),
795
        }
796
        assert len(sm.get_all_anonymous_mappings(s1, s2)) == 2
1✔
797

798
        # test include_dist
799
        dists = {Element("N"): 0, Element("P"): 0.0010725064}
1✔
800
        for mapping, d in sm.get_all_anonymous_mappings(s1, s2, include_dist=True):
1✔
801
            assert dists[mapping[Element("As")]] == approx(d)
1✔
802

803
    def test_rms_vs_minimax(self):
1✔
804
        # This tests that structures with adjusted RMS less than stol, but minimax
805
        # greater than stol are treated properly
806
        # stol=0.3 gives exactly an ftol of 0.1 on the c axis
807
        sm = StructureMatcher(ltol=0.2, stol=0.301, angle_tol=1, primitive_cell=False)
1✔
808
        l = Lattice.orthorhombic(1, 2, 12)
1✔
809

810
        sp = ["Si", "Si", "Al"]
1✔
811
        s1 = Structure(l, sp, [[0.5, 0, 0], [0, 0, 0], [0, 0, 0.5]])
1✔
812
        s2 = Structure(l, sp, [[0.5, 0, 0], [0, 0, 0], [0, 0, 0.6]])
1✔
813
        self.assertArrayAlmostEqual(sm.get_rms_dist(s1, s2), (0.32**0.5 / 2, 0.4))
1✔
814

815
        assert sm.fit(s1, s2) is False
1✔
816
        assert sm.fit_anonymous(s1, s2) is False
1✔
817
        assert sm.get_mapping(s1, s2) is None
1✔
818

819

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