• 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

85.61
/pymatgen/symmetry/kpath.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
Provides classes for generating high-symmetry k-paths using different conventions.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import abc
1✔
11
import itertools
1✔
12
import operator
1✔
13
from math import ceil, cos, e, pi, sin, tan
1✔
14
from typing import Any
1✔
15
from warnings import warn
1✔
16

17
import networkx as nx
1✔
18
import numpy as np
1✔
19
import spglib
1✔
20
from monty.dev import requires
1✔
21
from scipy.linalg import sqrtm
1✔
22

23
from pymatgen.core.lattice import Lattice
1✔
24
from pymatgen.core.operations import MagSymmOp, SymmOp
1✔
25
from pymatgen.core.structure import Structure
1✔
26
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
27
from pymatgen.util.typing import SpeciesLike
1✔
28

29
try:
1✔
30
    from seekpath import get_path
1✔
31
except ImportError:
×
32
    get_path = None
×
33

34
__author__ = "Geoffroy Hautier, Katherine Latimer, Jason Munro"
1✔
35
__copyright__ = "Copyright 2020, The Materials Project"
1✔
36
__version__ = "0.1"
1✔
37
__maintainer__ = "Jason Munro"
1✔
38
__email__ = "jmunro@lbl.gov"
1✔
39
__status__ = "Development"
1✔
40
__date__ = "March 2020"
1✔
41

42

43
class KPathBase(metaclass=abc.ABCMeta):
1✔
44
    """
45
    This is the base class for classes used to generate high-symmetry
46
    paths in reciprocal space (k-paths) for band structure calculations.
47
    """
48

49
    @abc.abstractmethod
1✔
50
    def __init__(self, structure: Structure, symprec: float = 0.01, angle_tolerance=5, atol=1e-5, *args, **kwargs):
1✔
51
        """
52
        Args:
53
        structure (Structure): Structure object.
54
        symprec (float): Tolerance for symmetry finding.
55
        angle_tolerance (float): Angle tolerance for symmetry finding.
56
        atol (float): Absolute tolerance used to compare structures
57
            and determine symmetric equivalence of points and lines
58
            in the BZ.
59
        """
60
        self._structure = structure
1✔
61
        self._latt = self._structure.lattice
1✔
62
        self._rec_lattice = self._structure.lattice.reciprocal_lattice
1✔
63
        self._kpath: dict[str, Any] | None = None
1✔
64

65
        self._symprec = symprec
1✔
66
        self._atol = atol
1✔
67
        self._angle_tolerance = angle_tolerance
1✔
68

69
    @property
1✔
70
    def structure(self):
1✔
71
        """
72
        Returns:
73
        The input structure
74
        """
75
        return self._structure
×
76

77
    @property
1✔
78
    def lattice(self):
1✔
79
        """
80
        Returns:
81
        The real space lattice
82
        """
83
        return self._latt
×
84

85
    @property
1✔
86
    def rec_lattice(self):
1✔
87
        """
88
        Returns:
89
        The reciprocal space lattice
90
        """
91
        return self._rec_lattice
×
92

93
    @property
1✔
94
    def kpath(self):
1✔
95
        """
96
        Returns:
97
        The symmetry line path in reciprocal space
98
        """
99
        return self._kpath
1✔
100

101
    def get_kpoints(self, line_density=20, coords_are_cartesian=True):
1✔
102
        """
103
        Returns:
104
        kpoints along the path in Cartesian coordinates
105
        together with the critical-point labels.
106
        """
107
        list_k_points = []
1✔
108
        sym_point_labels = []
1✔
109
        for b in self.kpath["path"]:
1✔
110
            for i in range(1, len(b)):
1✔
111
                start = np.array(self.kpath["kpoints"][b[i - 1]])
1✔
112
                end = np.array(self.kpath["kpoints"][b[i]])
1✔
113
                distance = np.linalg.norm(
1✔
114
                    self._rec_lattice.get_cartesian_coords(start) - self._rec_lattice.get_cartesian_coords(end)
115
                )
116
                nb = int(ceil(distance * line_density))
1✔
117
                if nb == 0:
1✔
118
                    continue
×
119
                sym_point_labels.extend([b[i - 1]] + [""] * (nb - 1) + [b[i]])
1✔
120
                list_k_points.extend(
1✔
121
                    [
122
                        self._rec_lattice.get_cartesian_coords(start)
123
                        + float(i)
124
                        / float(nb)
125
                        * (self._rec_lattice.get_cartesian_coords(end) - self._rec_lattice.get_cartesian_coords(start))
126
                        for i in range(0, nb + 1)
127
                    ]
128
                )
129
        if coords_are_cartesian:
1✔
130
            return list_k_points, sym_point_labels
1✔
131

132
        frac_k_points = [self._rec_lattice.get_fractional_coords(k) for k in list_k_points]
1✔
133
        return frac_k_points, sym_point_labels
1✔
134

135

136
class KPathSetyawanCurtarolo(KPathBase):
1✔
137
    """
138
    This class looks for a path along high-symmetry lines in
139
    the Brillouin zone.
140
    It is based on Setyawan, W., & Curtarolo, S. (2010).
141
    High-throughput electronic band structure calculations:
142
    Challenges and tools. Computational Materials Science,
143
    49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010
144
    It should be used with primitive structures that
145
    comply with the definition given in the paper.
146
    The symmetry is determined by spglib using the
147
    SpacegroupAnalyzer class. The analyzer can be used to
148
    produce the correct primitive structure with the method
149
    get_primitive_standard_structure(international_monoclinic=False).
150
    A warning will signal possible compatibility problems
151
    with the given structure. k-points generated using the get_kpoints() method
152
    are returned for the reciprocal cell basis defined in the paper.
153
    """
154

155
    def __init__(self, structure: Structure, symprec: float = 0.01, angle_tolerance=5, atol=1e-5):
1✔
156
        """
157
        Args:
158
        structure (Structure): Structure object.
159
        symprec (float): Tolerance for symmetry finding.
160
        angle_tolerance (float): Angle tolerance for symmetry finding.
161
        atol (float): Absolute tolerance used to compare the input
162
            structure with the one expected as primitive standard.
163
            A warning will be issued if the cells don't match.
164
        """
165
        if "magmom" in structure.site_properties:
1✔
166
            warn(
×
167
                "'magmom' entry found in site properties but will be ignored \
168
                  for the Setyawan and Curtarolo convention."
169
            )
170

171
        super().__init__(structure, symprec=symprec, angle_tolerance=angle_tolerance, atol=atol)
1✔
172

173
        self._sym = SpacegroupAnalyzer(structure, symprec=symprec, angle_tolerance=angle_tolerance)
1✔
174
        self._prim = self._sym.get_primitive_standard_structure(international_monoclinic=False)
1✔
175
        self._conv = self._sym.get_conventional_standard_structure(international_monoclinic=False)
1✔
176
        self._rec_lattice = self._prim.lattice.reciprocal_lattice
1✔
177

178
        # Note: this warning will be issued for space groups 38-41, since the primitive cell must be
179
        # reformatted to match the Setyawan/Curtarolo convention in order to work with the current k-path
180
        # generation scheme.
181
        if not np.allclose(self._structure.lattice.matrix, self._prim.lattice.matrix, atol=atol):
1✔
182
            warn(
1✔
183
                "The input structure does not match the expected standard primitive! "
184
                "The path may be incorrect. Use at your own risk."
185
            )
186

187
        lattice_type = self._sym.get_lattice_type()
1✔
188
        spg_symbol = self._sym.get_space_group_symbol()
1✔
189

190
        if lattice_type == "cubic":
1✔
191
            if "P" in spg_symbol:
1✔
192
                self._kpath = self.cubic()
1✔
193
            elif "F" in spg_symbol:
1✔
194
                self._kpath = self.fcc()
1✔
195
            elif "I" in spg_symbol:
1✔
196
                self._kpath = self.bcc()
1✔
197
            else:
198
                warn(f"Unexpected value for spg_symbol: {spg_symbol}")
×
199

200
        elif lattice_type == "tetragonal":
1✔
201
            if "P" in spg_symbol:
1✔
202
                self._kpath = self.tet()
1✔
203
            elif "I" in spg_symbol:
1✔
204
                a = self._conv.lattice.abc[0]
1✔
205
                c = self._conv.lattice.abc[2]
1✔
206
                if c < a:
1✔
207
                    self._kpath = self.bctet1(c, a)
×
208
                else:
209
                    self._kpath = self.bctet2(c, a)
1✔
210
            else:
211
                warn(f"Unexpected value for spg_symbol: {spg_symbol}")
×
212

213
        elif lattice_type == "orthorhombic":
1✔
214
            a = self._conv.lattice.abc[0]
1✔
215
            b = self._conv.lattice.abc[1]
1✔
216
            c = self._conv.lattice.abc[2]
1✔
217

218
            if "P" in spg_symbol:
1✔
219
                self._kpath = self.orc()
1✔
220

221
            elif "F" in spg_symbol:
1✔
222
                if 1 / a**2 > 1 / b**2 + 1 / c**2:
1✔
223
                    self._kpath = self.orcf1(a, b, c)
1✔
224
                elif 1 / a**2 < 1 / b**2 + 1 / c**2:
×
225
                    self._kpath = self.orcf2(a, b, c)
×
226
                else:
227
                    self._kpath = self.orcf3(a, b, c)
×
228

229
            elif "I" in spg_symbol:
1✔
230
                self._kpath = self.orci(a, b, c)
1✔
231

232
            elif "C" in spg_symbol or "A" in spg_symbol:
1✔
233
                self._kpath = self.orcc(a, b, c)
1✔
234
            else:
235
                warn(f"Unexpected value for spg_symbol: {spg_symbol}")
×
236

237
        elif lattice_type == "hexagonal":
1✔
238
            self._kpath = self.hex()
1✔
239

240
        elif lattice_type == "rhombohedral":
1✔
241
            alpha = self._prim.lattice.parameters[3]
1✔
242
            if alpha < 90:
1✔
243
                self._kpath = self.rhl1(alpha * pi / 180)
1✔
244
            else:
245
                self._kpath = self.rhl2(alpha * pi / 180)
×
246

247
        elif lattice_type == "monoclinic":
1✔
248
            a, b, c = self._conv.lattice.abc
1✔
249
            alpha = self._conv.lattice.parameters[3]
1✔
250
            # beta = self._conv.lattice.parameters[4]
251

252
            if "P" in spg_symbol:
1✔
253
                self._kpath = self.mcl(b, c, alpha * pi / 180)
1✔
254

255
            elif "C" in spg_symbol:
1✔
256
                kgamma = self._rec_lattice.parameters[5]
1✔
257
                if kgamma > 90:
1✔
258
                    self._kpath = self.mclc1(a, b, c, alpha * pi / 180)
×
259
                if kgamma == 90:
1✔
260
                    self._kpath = self.mclc2(a, b, c, alpha * pi / 180)
×
261
                if kgamma < 90:
1✔
262
                    if b * cos(alpha * pi / 180) / c + b**2 * sin(alpha * pi / 180) ** 2 / a**2 < 1:
1✔
263
                        self._kpath = self.mclc3(a, b, c, alpha * pi / 180)
1✔
264
                    if b * cos(alpha * pi / 180) / c + b**2 * sin(alpha * pi / 180) ** 2 / a**2 == 1:
1✔
265
                        self._kpath = self.mclc4(a, b, c, alpha * pi / 180)
×
266
                    if b * cos(alpha * pi / 180) / c + b**2 * sin(alpha * pi / 180) ** 2 / a**2 > 1:
1✔
267
                        self._kpath = self.mclc5(a, b, c, alpha * pi / 180)
1✔
268
            else:
269
                warn(f"Unexpected value for spg_symbol: {spg_symbol}")
×
270

271
        elif lattice_type == "triclinic":
1✔
272
            kalpha = self._rec_lattice.parameters[3]
1✔
273
            kbeta = self._rec_lattice.parameters[4]
1✔
274
            kgamma = self._rec_lattice.parameters[5]
1✔
275
            if kalpha > 90 and kbeta > 90 and kgamma > 90:
1✔
276
                self._kpath = self.tria()
1✔
277
            if kalpha < 90 and kbeta < 90 and kgamma < 90:
1✔
278
                self._kpath = self.trib()
1✔
279
            if kalpha > 90 and kbeta > 90 and kgamma == 90:
1✔
280
                self._kpath = self.tria()
×
281
            if kalpha < 90 and kbeta < 90 and kgamma == 90:
1✔
282
                self._kpath = self.trib()
×
283

284
        else:
285
            warn(f"Unknown lattice type {lattice_type}")
×
286

287
    @property
1✔
288
    def conventional(self):
1✔
289
        """
290
        Returns:
291
            The conventional cell structure
292
        """
293
        return self._conv
1✔
294

295
    @property
1✔
296
    def prim(self):
1✔
297
        """
298
        Returns:
299
            The primitive cell structure
300
        """
301
        return self._prim
1✔
302

303
    @property
1✔
304
    def prim_rec(self):
1✔
305
        """
306
        Returns:
307
            The primitive reciprocal cell structure
308
        """
309
        return self._rec_lattice
1✔
310

311
    def cubic(self):
1✔
312
        """
313
        CUB Path
314
        """
315
        self.name = "CUB"
1✔
316
        kpoints = {
1✔
317
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
318
            "X": np.array([0.0, 0.5, 0.0]),
319
            "R": np.array([0.5, 0.5, 0.5]),
320
            "M": np.array([0.5, 0.5, 0.0]),
321
        }
322
        path = [["\\Gamma", "X", "M", "\\Gamma", "R", "X"], ["M", "R"]]
1✔
323
        return {"kpoints": kpoints, "path": path}
1✔
324

325
    def fcc(self):
1✔
326
        """
327
        FCC Path
328
        """
329
        self.name = "FCC"
1✔
330
        kpoints = {
1✔
331
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
332
            "K": np.array([3.0 / 8.0, 3.0 / 8.0, 3.0 / 4.0]),
333
            "L": np.array([0.5, 0.5, 0.5]),
334
            "U": np.array([5.0 / 8.0, 1.0 / 4.0, 5.0 / 8.0]),
335
            "W": np.array([0.5, 1.0 / 4.0, 3.0 / 4.0]),
336
            "X": np.array([0.5, 0.0, 0.5]),
337
        }
338
        path = [
1✔
339
            ["\\Gamma", "X", "W", "K", "\\Gamma", "L", "U", "W", "L", "K"],
340
            ["U", "X"],
341
        ]
342
        return {"kpoints": kpoints, "path": path}
1✔
343

344
    def bcc(self):
1✔
345
        """
346
        BCC Path
347
        """
348
        self.name = "BCC"
1✔
349
        kpoints = {
1✔
350
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
351
            "H": np.array([0.5, -0.5, 0.5]),
352
            "P": np.array([0.25, 0.25, 0.25]),
353
            "N": np.array([0.0, 0.0, 0.5]),
354
        }
355
        path = [["\\Gamma", "H", "N", "\\Gamma", "P", "H"], ["P", "N"]]
1✔
356
        return {"kpoints": kpoints, "path": path}
1✔
357

358
    def tet(self):
1✔
359
        """
360
        TET Path
361
        """
362
        self.name = "TET"
1✔
363
        kpoints = {
1✔
364
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
365
            "A": np.array([0.5, 0.5, 0.5]),
366
            "M": np.array([0.5, 0.5, 0.0]),
367
            "R": np.array([0.0, 0.5, 0.5]),
368
            "X": np.array([0.0, 0.5, 0.0]),
369
            "Z": np.array([0.0, 0.0, 0.5]),
370
        }
371
        path = [
1✔
372
            ["\\Gamma", "X", "M", "\\Gamma", "Z", "R", "A", "Z"],
373
            ["X", "R"],
374
            ["M", "A"],
375
        ]
376
        return {"kpoints": kpoints, "path": path}
1✔
377

378
    def bctet1(self, c, a):
1✔
379
        """
380
        BCT1 Path
381
        """
382
        self.name = "BCT1"
×
383
        eta = (1 + c**2 / a**2) / 4.0
×
384
        kpoints = {
×
385
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
386
            "M": np.array([-0.5, 0.5, 0.5]),
387
            "N": np.array([0.0, 0.5, 0.0]),
388
            "P": np.array([0.25, 0.25, 0.25]),
389
            "X": np.array([0.0, 0.0, 0.5]),
390
            "Z": np.array([eta, eta, -eta]),
391
            "Z_1": np.array([-eta, 1 - eta, eta]),
392
        }
393
        path = [["\\Gamma", "X", "M", "\\Gamma", "Z", "P", "N", "Z_1", "M"], ["X", "P"]]
×
394
        return {"kpoints": kpoints, "path": path}
×
395

396
    def bctet2(self, c, a):
1✔
397
        """
398
        BCT2 Path
399
        """
400
        self.name = "BCT2"
1✔
401
        eta = (1 + a**2 / c**2) / 4.0
1✔
402
        zeta = a**2 / (2 * c**2)
1✔
403
        kpoints = {
1✔
404
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
405
            "N": np.array([0.0, 0.5, 0.0]),
406
            "P": np.array([0.25, 0.25, 0.25]),
407
            "\\Sigma": np.array([-eta, eta, eta]),
408
            "\\Sigma_1": np.array([eta, 1 - eta, -eta]),
409
            "X": np.array([0.0, 0.0, 0.5]),
410
            "Y": np.array([-zeta, zeta, 0.5]),
411
            "Y_1": np.array([0.5, 0.5, -zeta]),
412
            "Z": np.array([0.5, 0.5, -0.5]),
413
        }
414
        path = [
1✔
415
            [
416
                "\\Gamma",
417
                "X",
418
                "Y",
419
                "\\Sigma",
420
                "\\Gamma",
421
                "Z",
422
                "\\Sigma_1",
423
                "N",
424
                "P",
425
                "Y_1",
426
                "Z",
427
            ],
428
            ["X", "P"],
429
        ]
430
        return {"kpoints": kpoints, "path": path}
1✔
431

432
    def orc(self):
1✔
433
        """
434
        ORC Path
435
        """
436
        self.name = "ORC"
1✔
437
        kpoints = {
1✔
438
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
439
            "R": np.array([0.5, 0.5, 0.5]),
440
            "S": np.array([0.5, 0.5, 0.0]),
441
            "T": np.array([0.0, 0.5, 0.5]),
442
            "U": np.array([0.5, 0.0, 0.5]),
443
            "X": np.array([0.5, 0.0, 0.0]),
444
            "Y": np.array([0.0, 0.5, 0.0]),
445
            "Z": np.array([0.0, 0.0, 0.5]),
446
        }
447
        path = [
1✔
448
            ["\\Gamma", "X", "S", "Y", "\\Gamma", "Z", "U", "R", "T", "Z"],
449
            ["Y", "T"],
450
            ["U", "X"],
451
            ["S", "R"],
452
        ]
453
        return {"kpoints": kpoints, "path": path}
1✔
454

455
    def orcf1(self, a, b, c):
1✔
456
        """
457
        ORFC1 Path
458
        """
459
        self.name = "ORCF1"
1✔
460
        zeta = (1 + a**2 / b**2 - a**2 / c**2) / 4
1✔
461
        eta = (1 + a**2 / b**2 + a**2 / c**2) / 4
1✔
462

463
        kpoints = {
1✔
464
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
465
            "A": np.array([0.5, 0.5 + zeta, zeta]),
466
            "A_1": np.array([0.5, 0.5 - zeta, 1 - zeta]),
467
            "L": np.array([0.5, 0.5, 0.5]),
468
            "T": np.array([1, 0.5, 0.5]),
469
            "X": np.array([0.0, eta, eta]),
470
            "X_1": np.array([1, 1 - eta, 1 - eta]),
471
            "Y": np.array([0.5, 0.0, 0.5]),
472
            "Z": np.array([0.5, 0.5, 0.0]),
473
        }
474
        path = [
1✔
475
            ["\\Gamma", "Y", "T", "Z", "\\Gamma", "X", "A_1", "Y"],
476
            ["T", "X_1"],
477
            ["X", "A", "Z"],
478
            ["L", "\\Gamma"],
479
        ]
480
        return {"kpoints": kpoints, "path": path}
1✔
481

482
    def orcf2(self, a, b, c):
1✔
483
        """
484
        ORFC2 Path
485
        """
486
        self.name = "ORCF2"
×
487
        phi = (1 + c**2 / b**2 - c**2 / a**2) / 4
×
488
        eta = (1 + a**2 / b**2 - a**2 / c**2) / 4
×
489
        delta = (1 + b**2 / a**2 - b**2 / c**2) / 4
×
490
        kpoints = {
×
491
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
492
            "C": np.array([0.5, 0.5 - eta, 1 - eta]),
493
            "C_1": np.array([0.5, 0.5 + eta, eta]),
494
            "D": np.array([0.5 - delta, 0.5, 1 - delta]),
495
            "D_1": np.array([0.5 + delta, 0.5, delta]),
496
            "L": np.array([0.5, 0.5, 0.5]),
497
            "H": np.array([1 - phi, 0.5 - phi, 0.5]),
498
            "H_1": np.array([phi, 0.5 + phi, 0.5]),
499
            "X": np.array([0.0, 0.5, 0.5]),
500
            "Y": np.array([0.5, 0.0, 0.5]),
501
            "Z": np.array([0.5, 0.5, 0.0]),
502
        }
503
        path = [
×
504
            ["\\Gamma", "Y", "C", "D", "X", "\\Gamma", "Z", "D_1", "H", "C"],
505
            ["C_1", "Z"],
506
            ["X", "H_1"],
507
            ["H", "Y"],
508
            ["L", "\\Gamma"],
509
        ]
510
        return {"kpoints": kpoints, "path": path}
×
511

512
    def orcf3(self, a, b, c):
1✔
513
        """
514
        ORFC3 Path
515
        """
516
        self.name = "ORCF3"
×
517
        zeta = (1 + a**2 / b**2 - a**2 / c**2) / 4
×
518
        eta = (1 + a**2 / b**2 + a**2 / c**2) / 4
×
519
        kpoints = {
×
520
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
521
            "A": np.array([0.5, 0.5 + zeta, zeta]),
522
            "A_1": np.array([0.5, 0.5 - zeta, 1 - zeta]),
523
            "L": np.array([0.5, 0.5, 0.5]),
524
            "T": np.array([1, 0.5, 0.5]),
525
            "X": np.array([0.0, eta, eta]),
526
            "X_1": np.array([1, 1 - eta, 1 - eta]),
527
            "Y": np.array([0.5, 0.0, 0.5]),
528
            "Z": np.array([0.5, 0.5, 0.0]),
529
        }
530
        path = [
×
531
            ["\\Gamma", "Y", "T", "Z", "\\Gamma", "X", "A_1", "Y"],
532
            ["X", "A", "Z"],
533
            ["L", "\\Gamma"],
534
        ]
535
        return {"kpoints": kpoints, "path": path}
×
536

537
    def orci(self, a, b, c):
1✔
538
        """
539
        ORCI Path
540
        """
541
        self.name = "ORCI"
1✔
542
        zeta = (1 + a**2 / c**2) / 4
1✔
543
        eta = (1 + b**2 / c**2) / 4
1✔
544
        delta = (b**2 - a**2) / (4 * c**2)
1✔
545
        mu = (a**2 + b**2) / (4 * c**2)
1✔
546
        kpoints = {
1✔
547
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
548
            "L": np.array([-mu, mu, 0.5 - delta]),
549
            "L_1": np.array([mu, -mu, 0.5 + delta]),
550
            "L_2": np.array([0.5 - delta, 0.5 + delta, -mu]),
551
            "R": np.array([0.0, 0.5, 0.0]),
552
            "S": np.array([0.5, 0.0, 0.0]),
553
            "T": np.array([0.0, 0.0, 0.5]),
554
            "W": np.array([0.25, 0.25, 0.25]),
555
            "X": np.array([-zeta, zeta, zeta]),
556
            "X_1": np.array([zeta, 1 - zeta, -zeta]),
557
            "Y": np.array([eta, -eta, eta]),
558
            "Y_1": np.array([1 - eta, eta, -eta]),
559
            "Z": np.array([0.5, 0.5, -0.5]),
560
        }
561
        path = [
1✔
562
            ["\\Gamma", "X", "L", "T", "W", "R", "X_1", "Z", "\\Gamma", "Y", "S", "W"],
563
            ["L_1", "Y"],
564
            ["Y_1", "Z"],
565
        ]
566
        return {"kpoints": kpoints, "path": path}
1✔
567

568
    def orcc(self, a, b, c):
1✔
569
        """
570
        ORCC Path
571
        """
572
        self.name = "ORCC"
1✔
573
        zeta = (1 + a**2 / b**2) / 4
1✔
574
        kpoints = {
1✔
575
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
576
            "A": np.array([zeta, zeta, 0.5]),
577
            "A_1": np.array([-zeta, 1 - zeta, 0.5]),
578
            "R": np.array([0.0, 0.5, 0.5]),
579
            "S": np.array([0.0, 0.5, 0.0]),
580
            "T": np.array([-0.5, 0.5, 0.5]),
581
            "X": np.array([zeta, zeta, 0.0]),
582
            "X_1": np.array([-zeta, 1 - zeta, 0.0]),
583
            "Y": np.array([-0.5, 0.5, 0]),
584
            "Z": np.array([0.0, 0.0, 0.5]),
585
        }
586
        path = [
1✔
587
            [
588
                "\\Gamma",
589
                "X",
590
                "S",
591
                "R",
592
                "A",
593
                "Z",
594
                "\\Gamma",
595
                "Y",
596
                "X_1",
597
                "A_1",
598
                "T",
599
                "Y",
600
            ],
601
            ["Z", "T"],
602
        ]
603
        return {"kpoints": kpoints, "path": path}
1✔
604

605
    def hex(self):
1✔
606
        """
607
        HEX Path
608
        """
609
        self.name = "HEX"
1✔
610
        kpoints = {
1✔
611
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
612
            "A": np.array([0.0, 0.0, 0.5]),
613
            "H": np.array([1.0 / 3.0, 1.0 / 3.0, 0.5]),
614
            "K": np.array([1.0 / 3.0, 1.0 / 3.0, 0.0]),
615
            "L": np.array([0.5, 0.0, 0.5]),
616
            "M": np.array([0.5, 0.0, 0.0]),
617
        }
618
        path = [
1✔
619
            ["\\Gamma", "M", "K", "\\Gamma", "A", "L", "H", "A"],
620
            ["L", "M"],
621
            ["K", "H"],
622
        ]
623
        return {"kpoints": kpoints, "path": path}
1✔
624

625
    def rhl1(self, alpha):
1✔
626
        """
627
        RHL1 Path
628
        """
629
        self.name = "RHL1"
1✔
630
        eta = (1 + 4 * cos(alpha)) / (2 + 4 * cos(alpha))
1✔
631
        nu = 3.0 / 4.0 - eta / 2.0
1✔
632
        kpoints = {
1✔
633
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
634
            "B": np.array([eta, 0.5, 1.0 - eta]),
635
            "B_1": np.array([1.0 / 2.0, 1.0 - eta, eta - 1.0]),
636
            "F": np.array([0.5, 0.5, 0.0]),
637
            "L": np.array([0.5, 0.0, 0.0]),
638
            "L_1": np.array([0.0, 0.0, -0.5]),
639
            "P": np.array([eta, nu, nu]),
640
            "P_1": np.array([1.0 - nu, 1.0 - nu, 1.0 - eta]),
641
            "P_2": np.array([nu, nu, eta - 1.0]),
642
            "Q": np.array([1.0 - nu, nu, 0.0]),
643
            "X": np.array([nu, 0.0, -nu]),
644
            "Z": np.array([0.5, 0.5, 0.5]),
645
        }
646
        path = [
1✔
647
            ["\\Gamma", "L", "B_1"],
648
            ["B", "Z", "\\Gamma", "X"],
649
            ["Q", "F", "P_1", "Z"],
650
            ["L", "P"],
651
        ]
652
        return {"kpoints": kpoints, "path": path}
1✔
653

654
    def rhl2(self, alpha):
1✔
655
        """
656
        RHL2 Path
657
        """
658
        self.name = "RHL2"
×
659
        eta = 1 / (2 * tan(alpha / 2.0) ** 2)
×
660
        nu = 3.0 / 4.0 - eta / 2.0
×
661
        kpoints = {
×
662
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
663
            "F": np.array([0.5, -0.5, 0.0]),
664
            "L": np.array([0.5, 0.0, 0.0]),
665
            "P": np.array([1 - nu, -nu, 1 - nu]),
666
            "P_1": np.array([nu, nu - 1.0, nu - 1.0]),
667
            "Q": np.array([eta, eta, eta]),
668
            "Q_1": np.array([1.0 - eta, -eta, -eta]),
669
            "Z": np.array([0.5, -0.5, 0.5]),
670
        }
671
        path = [["\\Gamma", "P", "Z", "Q", "\\Gamma", "F", "P_1", "Q_1", "L", "Z"]]
×
672
        return {"kpoints": kpoints, "path": path}
×
673

674
    def mcl(self, b, c, beta):
1✔
675
        """
676
        MCL Path
677
        """
678
        self.name = "MCL"
1✔
679
        eta = (1 - b * cos(beta) / c) / (2 * sin(beta) ** 2)
1✔
680
        nu = 0.5 - eta * c * cos(beta) / b
1✔
681
        kpoints = {
1✔
682
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
683
            "A": np.array([0.5, 0.5, 0.0]),
684
            "C": np.array([0.0, 0.5, 0.5]),
685
            "D": np.array([0.5, 0.0, 0.5]),
686
            "D_1": np.array([0.5, 0.5, -0.5]),
687
            "E": np.array([0.5, 0.5, 0.5]),
688
            "H": np.array([0.0, eta, 1.0 - nu]),
689
            "H_1": np.array([0.0, 1.0 - eta, nu]),
690
            "H_2": np.array([0.0, eta, -nu]),
691
            "M": np.array([0.5, eta, 1.0 - nu]),
692
            "M_1": np.array([0.5, 1 - eta, nu]),
693
            "M_2": np.array([0.5, 1 - eta, nu]),
694
            "X": np.array([0.0, 0.5, 0.0]),
695
            "Y": np.array([0.0, 0.0, 0.5]),
696
            "Y_1": np.array([0.0, 0.0, -0.5]),
697
            "Z": np.array([0.5, 0.0, 0.0]),
698
        }
699
        path = [
1✔
700
            ["\\Gamma", "Y", "H", "C", "E", "M_1", "A", "X", "H_1"],
701
            ["M", "D", "Z"],
702
            ["Y", "D"],
703
        ]
704
        return {"kpoints": kpoints, "path": path}
1✔
705

706
    def mclc1(self, a, b, c, alpha):
1✔
707
        """
708
        MCLC1 Path
709
        """
710
        self.name = "MCLC1"
×
711
        zeta = (2 - b * cos(alpha) / c) / (4 * sin(alpha) ** 2)
×
712
        eta = 0.5 + 2 * zeta * c * cos(alpha) / b
×
713
        psi = 0.75 - a**2 / (4 * b**2 * sin(alpha) ** 2)
×
714
        phi = psi + (0.75 - psi) * b * cos(alpha) / c
×
715
        kpoints = {
×
716
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
717
            "N": np.array([0.5, 0.0, 0.0]),
718
            "N_1": np.array([0.0, -0.5, 0.0]),
719
            "F": np.array([1 - zeta, 1 - zeta, 1 - eta]),
720
            "F_1": np.array([zeta, zeta, eta]),
721
            "F_2": np.array([-zeta, -zeta, 1 - eta]),
722
            "I": np.array([phi, 1 - phi, 0.5]),
723
            "I_1": np.array([1 - phi, phi - 1, 0.5]),
724
            "L": np.array([0.5, 0.5, 0.5]),
725
            "M": np.array([0.5, 0.0, 0.5]),
726
            "X": np.array([1 - psi, psi - 1, 0.0]),
727
            "X_1": np.array([psi, 1 - psi, 0.0]),
728
            "X_2": np.array([psi - 1, -psi, 0.0]),
729
            "Y": np.array([0.5, 0.5, 0.0]),
730
            "Y_1": np.array([-0.5, -0.5, 0.0]),
731
            "Z": np.array([0.0, 0.0, 0.5]),
732
        }
733
        path = [
×
734
            ["\\Gamma", "Y", "F", "L", "I"],
735
            ["I_1", "Z", "F_1"],
736
            ["Y", "X_1"],
737
            ["X", "\\Gamma", "N"],
738
            ["M", "\\Gamma"],
739
        ]
740
        return {"kpoints": kpoints, "path": path}
×
741

742
    def mclc2(self, a, b, c, alpha):
1✔
743
        """
744
        MCLC2 Path
745
        """
746
        self.name = "MCLC2"
×
747
        zeta = (2 - b * cos(alpha) / c) / (4 * sin(alpha) ** 2)
×
748
        eta = 0.5 + 2 * zeta * c * cos(alpha) / b
×
749
        psi = 0.75 - a**2 / (4 * b**2 * sin(alpha) ** 2)
×
750
        phi = psi + (0.75 - psi) * b * cos(alpha) / c
×
751
        kpoints = {
×
752
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
753
            "N": np.array([0.5, 0.0, 0.0]),
754
            "N_1": np.array([0.0, -0.5, 0.0]),
755
            "F": np.array([1 - zeta, 1 - zeta, 1 - eta]),
756
            "F_1": np.array([zeta, zeta, eta]),
757
            "F_2": np.array([-zeta, -zeta, 1 - eta]),
758
            "F_3": np.array([1 - zeta, -zeta, 1 - eta]),
759
            "I": np.array([phi, 1 - phi, 0.5]),
760
            "I_1": np.array([1 - phi, phi - 1, 0.5]),
761
            "L": np.array([0.5, 0.5, 0.5]),
762
            "M": np.array([0.5, 0.0, 0.5]),
763
            "X": np.array([1 - psi, psi - 1, 0.0]),
764
            "X_1": np.array([psi, 1 - psi, 0.0]),
765
            "X_2": np.array([psi - 1, -psi, 0.0]),
766
            "Y": np.array([0.5, 0.5, 0.0]),
767
            "Y_1": np.array([-0.5, -0.5, 0.0]),
768
            "Z": np.array([0.0, 0.0, 0.5]),
769
        }
770
        path = [
×
771
            ["\\Gamma", "Y", "F", "L", "I"],
772
            ["I_1", "Z", "F_1"],
773
            ["N", "\\Gamma", "M"],
774
        ]
775
        return {"kpoints": kpoints, "path": path}
×
776

777
    def mclc3(self, a, b, c, alpha):
1✔
778
        """
779
        MCLC3 Path
780
        """
781
        self.name = "MCLC3"
1✔
782
        mu = (1 + b**2 / a**2) / 4.0
1✔
783
        delta = b * c * cos(alpha) / (2 * a**2)
1✔
784
        zeta = mu - 0.25 + (1 - b * cos(alpha) / c) / (4 * sin(alpha) ** 2)
1✔
785
        eta = 0.5 + 2 * zeta * c * cos(alpha) / b
1✔
786
        phi = 1 + zeta - 2 * mu
1✔
787
        psi = eta - 2 * delta
1✔
788
        kpoints = {
1✔
789
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
790
            "F": np.array([1 - phi, 1 - phi, 1 - psi]),
791
            "F_1": np.array([phi, phi - 1, psi]),
792
            "F_2": np.array([1 - phi, -phi, 1 - psi]),
793
            "H": np.array([zeta, zeta, eta]),
794
            "H_1": np.array([1 - zeta, -zeta, 1 - eta]),
795
            "H_2": np.array([-zeta, -zeta, 1 - eta]),
796
            "I": np.array([0.5, -0.5, 0.5]),
797
            "M": np.array([0.5, 0.0, 0.5]),
798
            "N": np.array([0.5, 0.0, 0.0]),
799
            "N_1": np.array([0.0, -0.5, 0.0]),
800
            "X": np.array([0.5, -0.5, 0.0]),
801
            "Y": np.array([mu, mu, delta]),
802
            "Y_1": np.array([1 - mu, -mu, -delta]),
803
            "Y_2": np.array([-mu, -mu, -delta]),
804
            "Y_3": np.array([mu, mu - 1, delta]),
805
            "Z": np.array([0.0, 0.0, 0.5]),
806
        }
807
        path = [
1✔
808
            ["\\Gamma", "Y", "F", "H", "Z", "I", "F_1"],
809
            ["H_1", "Y_1", "X", "\\Gamma", "N"],
810
            ["M", "\\Gamma"],
811
        ]
812
        return {"kpoints": kpoints, "path": path}
1✔
813

814
    def mclc4(self, a, b, c, alpha):
1✔
815
        """
816
        MCLC4 Path
817
        """
818
        self.name = "MCLC4"
×
819
        mu = (1 + b**2 / a**2) / 4.0
×
820
        delta = b * c * cos(alpha) / (2 * a**2)
×
821
        zeta = mu - 0.25 + (1 - b * cos(alpha) / c) / (4 * sin(alpha) ** 2)
×
822
        eta = 0.5 + 2 * zeta * c * cos(alpha) / b
×
823
        phi = 1 + zeta - 2 * mu
×
824
        psi = eta - 2 * delta
×
825
        kpoints = {
×
826
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
827
            "F": np.array([1 - phi, 1 - phi, 1 - psi]),
828
            "F_1": np.array([phi, phi - 1, psi]),
829
            "F_2": np.array([1 - phi, -phi, 1 - psi]),
830
            "H": np.array([zeta, zeta, eta]),
831
            "H_1": np.array([1 - zeta, -zeta, 1 - eta]),
832
            "H_2": np.array([-zeta, -zeta, 1 - eta]),
833
            "I": np.array([0.5, -0.5, 0.5]),
834
            "M": np.array([0.5, 0.0, 0.5]),
835
            "N": np.array([0.5, 0.0, 0.0]),
836
            "N_1": np.array([0.0, -0.5, 0.0]),
837
            "X": np.array([0.5, -0.5, 0.0]),
838
            "Y": np.array([mu, mu, delta]),
839
            "Y_1": np.array([1 - mu, -mu, -delta]),
840
            "Y_2": np.array([-mu, -mu, -delta]),
841
            "Y_3": np.array([mu, mu - 1, delta]),
842
            "Z": np.array([0.0, 0.0, 0.5]),
843
        }
844
        path = [
×
845
            ["\\Gamma", "Y", "F", "H", "Z", "I"],
846
            ["H_1", "Y_1", "X", "\\Gamma", "N"],
847
            ["M", "\\Gamma"],
848
        ]
849
        return {"kpoints": kpoints, "path": path}
×
850

851
    def mclc5(self, a, b, c, alpha):
1✔
852
        """
853
        MCLC5 Path
854
        """
855
        self.name = "MCLC5"
1✔
856
        zeta = (b**2 / a**2 + (1 - b * cos(alpha) / c) / sin(alpha) ** 2) / 4
1✔
857
        eta = 0.5 + 2 * zeta * c * cos(alpha) / b
1✔
858
        mu = eta / 2 + b**2 / (4 * a**2) - b * c * cos(alpha) / (2 * a**2)
1✔
859
        nu = 2 * mu - zeta
1✔
860
        rho = 1 - zeta * a**2 / b**2
1✔
861
        omega = (4 * nu - 1 - b**2 * sin(alpha) ** 2 / a**2) * c / (2 * b * cos(alpha))
1✔
862
        delta = zeta * c * cos(alpha) / b + omega / 2 - 0.25
1✔
863
        kpoints = {
1✔
864
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
865
            "F": np.array([nu, nu, omega]),
866
            "F_1": np.array([1 - nu, 1 - nu, 1 - omega]),
867
            "F_2": np.array([nu, nu - 1, omega]),
868
            "H": np.array([zeta, zeta, eta]),
869
            "H_1": np.array([1 - zeta, -zeta, 1 - eta]),
870
            "H_2": np.array([-zeta, -zeta, 1 - eta]),
871
            "I": np.array([rho, 1 - rho, 0.5]),
872
            "I_1": np.array([1 - rho, rho - 1, 0.5]),
873
            "L": np.array([0.5, 0.5, 0.5]),
874
            "M": np.array([0.5, 0.0, 0.5]),
875
            "N": np.array([0.5, 0.0, 0.0]),
876
            "N_1": np.array([0.0, -0.5, 0.0]),
877
            "X": np.array([0.5, -0.5, 0.0]),
878
            "Y": np.array([mu, mu, delta]),
879
            "Y_1": np.array([1 - mu, -mu, -delta]),
880
            "Y_2": np.array([-mu, -mu, -delta]),
881
            "Y_3": np.array([mu, mu - 1, delta]),
882
            "Z": np.array([0.0, 0.0, 0.5]),
883
        }
884
        path = [
1✔
885
            ["\\Gamma", "Y", "F", "L", "I"],
886
            ["I_1", "Z", "H", "F_1"],
887
            ["H_1", "Y_1", "X", "\\Gamma", "N"],
888
            ["M", "\\Gamma"],
889
        ]
890
        return {"kpoints": kpoints, "path": path}
1✔
891

892
    def tria(self):
1✔
893
        """
894
        TRI1a Path
895
        """
896
        self.name = "TRI1a"
1✔
897
        kpoints = {
1✔
898
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
899
            "L": np.array([0.5, 0.5, 0.0]),
900
            "M": np.array([0.0, 0.5, 0.5]),
901
            "N": np.array([0.5, 0.0, 0.5]),
902
            "R": np.array([0.5, 0.5, 0.5]),
903
            "X": np.array([0.5, 0.0, 0.0]),
904
            "Y": np.array([0.0, 0.5, 0.0]),
905
            "Z": np.array([0.0, 0.0, 0.5]),
906
        }
907
        path = [
1✔
908
            ["X", "\\Gamma", "Y"],
909
            ["L", "\\Gamma", "Z"],
910
            ["N", "\\Gamma", "M"],
911
            ["R", "\\Gamma"],
912
        ]
913
        return {"kpoints": kpoints, "path": path}
1✔
914

915
    def trib(self):
1✔
916
        """
917
        TRI1b Path
918
        """
919
        self.name = "TRI1b"
1✔
920
        kpoints = {
1✔
921
            "\\Gamma": np.array([0.0, 0.0, 0.0]),
922
            "L": np.array([0.5, -0.5, 0.0]),
923
            "M": np.array([0.0, 0.0, 0.5]),
924
            "N": np.array([-0.5, -0.5, 0.5]),
925
            "R": np.array([0.0, -0.5, 0.5]),
926
            "X": np.array([0.0, -0.5, 0.0]),
927
            "Y": np.array([0.5, 0.0, 0.0]),
928
            "Z": np.array([-0.5, 0.0, 0.5]),
929
        }
930
        path = [
1✔
931
            ["X", "\\Gamma", "Y"],
932
            ["L", "\\Gamma", "Z"],
933
            ["N", "\\Gamma", "M"],
934
            ["R", "\\Gamma"],
935
        ]
936
        return {"kpoints": kpoints, "path": path}
1✔
937

938

939
class KPathSeek(KPathBase):
1✔
940
    """
941
    This class looks for a path along high-symmetry lines in the Brillouin zone. It is based on
942
    Hinuma, Y., Pizzi, G., Kumagai, Y., Oba, F., & Tanaka, I. (2017). Band structure diagram paths
943
    based on crystallography. Computational Materials Science, 128, 140-184.
944
    https://doi.org/10.1016/j.commatsci.2016.10.015. It should be used with primitive structures that
945
    comply with the definition given in the paper. The symmetry is determined by spglib using the
946
    SpacegroupAnalyzer class. k-points are generated using the get_kpoints() method for the
947
    reciprocal cell basis defined in the paper.
948
    """
949

950
    @requires(
1✔
951
        get_path is not None,
952
        "SeeK-path needs to be installed to use the convention of Hinuma et al. (2015)",
953
    )
954
    def __init__(self, structure: Structure, symprec: float = 0.01, angle_tolerance=5, atol=1e-5, system_is_tri=True):
1✔
955
        """
956
        Args:
957
            structure (Structure): Structure object
958
            symprec (float): Tolerance for symmetry finding
959
            angle_tolerance (float): Angle tolerance for symmetry finding.
960
            atol (float): Absolute tolerance used to determine edge cases
961
                for settings of structures.
962
            system_is_tri (bool): Indicates if the system is time-reversal
963
                invariant.
964
        """
965
        super().__init__(structure, symprec=symprec, angle_tolerance=angle_tolerance, atol=atol)
1✔
966

967
        positions = structure.frac_coords
1✔
968

969
        sp = structure.site_properties
1✔
970
        species = [site.species for site in structure]
1✔
971
        site_data = species
1✔
972

973
        if not system_is_tri:
1✔
974
            warn("Non-zero 'magmom' data will be used to define unique atoms in the cell.")
×
975
            site_data = zip(species, [tuple(vec) for vec in sp["magmom"]])  # type: ignore
×
976

977
        unique_species: list[SpeciesLike] = []
1✔
978
        numbers = []
1✔
979

980
        for species, g in itertools.groupby(site_data):
1✔
981
            if species in unique_species:
1✔
982
                ind = unique_species.index(species)
×
983
                numbers.extend([ind + 1] * len(tuple(g)))
×
984
            else:
985
                unique_species.append(species)
1✔
986
                numbers.extend([len(unique_species)] * len(tuple(g)))
1✔
987

988
        cell = (self._latt.matrix, positions, numbers)
1✔
989

990
        lattice, scale_pos, atom_num = spglib.standardize_cell(
1✔
991
            cell, to_primitive=False, no_idealize=True, symprec=symprec
992
        )
993

994
        spg_struct = (lattice, scale_pos, atom_num)
1✔
995
        spath_dat = get_path(spg_struct, system_is_tri, "hpkot", atol, symprec, angle_tolerance)
1✔
996

997
        self._tmat = self._trans_sc_to_Hin(spath_dat["bravais_lattice_extended"])
1✔
998
        self._rec_lattice = Lattice(spath_dat["reciprocal_primitive_lattice"])
1✔
999

1000
        spath_data_formatted = [[spath_dat["path"][0][0]]]
1✔
1001
        count = 0
1✔
1002
        for pnum in range(len(spath_dat["path"]) - 1):
1✔
1003
            if spath_dat["path"][pnum][1] == spath_dat["path"][pnum + 1][0]:
1✔
1004
                spath_data_formatted[count].append(spath_dat["path"][pnum][1])
1✔
1005
            else:
1006
                spath_data_formatted[count].append(spath_dat["path"][pnum][1])
1✔
1007
                spath_data_formatted.append([])
1✔
1008
                count += 1
1✔
1009
                spath_data_formatted[count].append(spath_dat["path"][pnum + 1][0])
1✔
1010

1011
        spath_data_formatted[-1].append(spath_dat["path"][-1][1])
1✔
1012

1013
        self._kpath = {
1✔
1014
            "kpoints": spath_dat["point_coords"],
1015
            "path": spath_data_formatted,
1016
        }
1017

1018
    @staticmethod
1✔
1019
    def _trans_sc_to_Hin(sub_class):
1✔
1020
        if sub_class in [
1✔
1021
            "cP1",
1022
            "cP2",
1023
            "cF1",
1024
            "cF2",
1025
            "cI1",
1026
            "tP1",
1027
            "oP1",
1028
            "hP1",
1029
            "hP2",
1030
            "tI1",
1031
            "tI2",
1032
            "oF1",
1033
            "oF3",
1034
            "oI1",
1035
            "oI3",
1036
            "oC1",
1037
            "hR1",
1038
            "hR2",
1039
            "aP1",
1040
            "aP2",
1041
            "aP3",
1042
            "oA1",
1043
        ]:
1044
            return np.eye(3)
1✔
1045
        if sub_class == "oF2":
1✔
1046
            return np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
1✔
1047
        if sub_class == "oI2":
1✔
1048
            return np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])
×
1049
        if sub_class == "oI3":
1✔
1050
            return np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
×
1051
        if sub_class == "oA2":
1✔
1052
            return np.array([[-1, 0, 0], [0, 1, 0], [0, 0, -1]])
1✔
1053
        if sub_class == "oC2":
1✔
1054
            return np.array([[-1, 0, 0], [0, 1, 0], [0, 0, -1]])
×
1055
        if sub_class in ["mP1", "mC1", "mC2", "mC3"]:
1✔
1056
            return np.array([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])
1✔
1057
        raise RuntimeError("Sub-classification of crystal not found!")
×
1058

1059

1060
class KPathLatimerMunro(KPathBase):
1✔
1061
    """
1062
    This class looks for a path along high-symmetry lines in the
1063
    Brillouin zone. It is based on the method outlined in:
1064
    npj Comput Mater 6, 112 (2020). 10.1038/s41524-020-00383-7
1065
    The user should ensure that the unit cell of the input structure
1066
    is as reduced as possible, i.e. that there is no linear
1067
    combination of lattice vectors which can produce a vector of
1068
    lesser magnitude than the given set (this is required to
1069
    obtain the correct Brillouin zone within the current
1070
    implementation). This is checked during initialization and a
1071
    warning is issued if the condition is not fulfilled.
1072
    In the case of magnetic structures, care must also be taken to
1073
    provide the magnetic primitive cell (i.e. that which reproduces
1074
    the entire crystal, including the correct magnetic ordering,
1075
    upon application of lattice translations). There is no algorithm to
1076
        check for this, so if the input structure is
1077
    incorrect, the class will output the incorrect k-path without
1078
    any warning being issued.
1079
    """
1080

1081
    def __init__(
1✔
1082
        self,
1083
        structure,
1084
        has_magmoms=False,
1085
        magmom_axis=None,
1086
        symprec=0.01,
1087
        angle_tolerance=5,
1088
        atol=1e-5,
1089
    ):
1090
        """
1091
        Args:
1092
            structure (Structure): Structure object
1093
            has_magmoms (bool): Whether the input structure contains
1094
                magnetic moments as site properties with the key 'magmom.'
1095
                Values may be in the form of 3-component vectors given in
1096
                the basis of the input lattice vectors, or as scalars, in
1097
                which case the spin axis will default to a_3, the third
1098
                real-space lattice vector (this triggers a warning).
1099
            magmom_axis (list or numpy array): 3-component vector specifying
1100
                direction along which magnetic moments given as scalars
1101
                should point. If all magnetic moments are provided as
1102
                vectors then this argument is not used.
1103
            symprec (float): Tolerance for symmetry finding
1104
            angle_tolerance (float): Angle tolerance for symmetry finding.
1105
            atol (float): Absolute tolerance used to determine symmetric
1106
                equivalence of points and lines in the BZ.
1107
        """
1108
        super().__init__(structure, symprec=symprec, angle_tolerance=angle_tolerance, atol=atol)
1✔
1109

1110
        # Check to see if input cell is reducible. Ref: B Gruber in Acta. Cryst. Vol. A29,
1111
        # pp. 433-440 ('The Relationship between Reduced Cells in a General Bravais lattice').
1112
        # The correct BZ will still be obtained if the lattice vectors are reducible by any
1113
        # linear combination of themselves with coefficients of absolute value less than 2,
1114
        # hence a missing factor of 2 as compared to the reference.
1115
        reducible = []
1✔
1116
        for i in range(3):
1✔
1117
            for j in range(3):
1✔
1118
                if i != j:
1✔
1119
                    if (
1✔
1120
                        np.absolute(np.dot(self._latt.matrix[i], self._latt.matrix[j]))
1121
                        > np.dot(self._latt.matrix[i], self._latt.matrix[i])
1122
                        and np.absolute(
1123
                            np.dot(self._latt.matrix[i], self._latt.matrix[j])
1124
                            - np.dot(self._latt.matrix[i], self._latt.matrix[i])
1125
                        )
1126
                        > atol
1127
                    ):
1128
                        reducible.append(True)
×
1129
                    else:
1130
                        reducible.append(False)
1✔
1131
        if np.any(reducible):
1✔
1132
            print("reducible")
×
1133
            warn(
×
1134
                "The unit cell of the input structure is not fully reduced!"
1135
                "The path may be incorrect. Use at your own risk."
1136
            )
1137

1138
        if magmom_axis is None:
1✔
1139
            magmom_axis = np.array([0, 0, 1])
1✔
1140
            axis_specified = False
1✔
1141
        else:
1142
            axis_specified = True
×
1143

1144
        self._kpath = self._get_ksymm_kpath(has_magmoms, magmom_axis, axis_specified, symprec, angle_tolerance, atol)
1✔
1145

1146
    @property
1✔
1147
    def mag_type(self):
1✔
1148
        """
1149
        Returns:
1150
        The type of magnetic space group as a string.
1151
        Current implementation does not distinguish
1152
        between types 3 and 4, so return value is '3/4'.
1153
        If has_magmoms is False, returns '0'.
1154
        """
1155
        return self._mag_type
×
1156

1157
    def _get_ksymm_kpath(self, has_magmoms, magmom_axis, axis_specified, symprec, angle_tolerance, atol):
1✔
1158
        ID = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
1✔
1159
        # parity, aka the inversion operation (not calling it
1160
        PAR = np.array([[-1, 0, 0], [0, -1, 0], [0, 0, -1]])
1✔
1161
        # INV to avoid confusion with np.linalg.inv() function)
1162

1163
        # 1: Get lattices of real and reciprocal structures, and reciprocal
1164
        # point group, and Brillouin zone (BZ)
1165

1166
        V = self._latt.matrix.T  # fractional real space to Cartesian real space
1✔
1167
        # fractional reciprocal space to Cartesian reciprocal space
1168
        W = self._rec_lattice.matrix.T
1✔
1169
        # fractional real space to fractional reciprocal space
1170
        A = np.dot(np.linalg.inv(W), V)
1✔
1171

1172
        if has_magmoms:
1✔
1173
            grey_struct = self._structure.copy()
1✔
1174
            grey_struct.remove_site_property("magmom")
1✔
1175
            sga = SpacegroupAnalyzer(grey_struct, symprec=symprec, angle_tolerance=angle_tolerance)
1✔
1176
            grey_ops = sga.get_symmetry_operations()
1✔
1177
            self._structure = self._convert_all_magmoms_to_vectors(magmom_axis, axis_specified)
1✔
1178
            mag_ops = self._get_magnetic_symmetry_operations(self._structure, grey_ops, atol)
1✔
1179

1180
            D = [
1✔
1181
                SymmOp.from_rotation_and_translation(
1182
                    rotation_matrix=op.rotation_matrix,
1183
                    translation_vec=op.translation_vector,
1184
                )
1185
                for op in mag_ops
1186
                if op.time_reversal == 1
1187
            ]
1188

1189
            fD = [
1✔
1190
                SymmOp.from_rotation_and_translation(
1191
                    rotation_matrix=op.rotation_matrix,
1192
                    translation_vec=op.translation_vector,
1193
                )
1194
                for op in mag_ops
1195
                if op.time_reversal == -1
1196
            ]
1197

1198
            if np.array([m == np.array([0, 0, 0]) for m in self._structure.site_properties["magmom"]]).all():
1✔
1199
                fD = D
×
1200
                D = []
×
1201

1202
            if len(fD) == 0:  # no operations contain time reversal; type 1
1✔
1203
                self._mag_type = "1"
×
1204
                isomorphic_point_group = [d.rotation_matrix for d in D]
×
1205
                recip_point_group = self._get_reciprocal_point_group(isomorphic_point_group, ID, A)
×
1206
            elif len(D) == 0:  # all operations contain time reversal / all magmoms zero; type 2
1✔
1207
                self._mag_type = "2"
×
1208
                isomorphic_point_group = [d.rotation_matrix for d in fD]
×
1209
                recip_point_group = self._get_reciprocal_point_group(isomorphic_point_group, PAR, A)
×
1210
            else:  # half and half; type 3 or 4
1211
                self._mag_type = "3/4"
1✔
1212
                f = self._get_coset_factor(D + fD, D)
1✔
1213
                isomorphic_point_group = [d.rotation_matrix for d in D]
1✔
1214
                recip_point_group = self._get_reciprocal_point_group(
1✔
1215
                    isomorphic_point_group, np.dot(PAR, f.rotation_matrix), A
1216
                )
1217
        else:
1218
            self._mag_type = "0"
1✔
1219
            if "magmom" in self._structure.site_properties:
1✔
1220
                warn(
×
1221
                    "The parameter has_magmoms is False, but site_properties contains the key magmom."
1222
                    "This property will be removed and could result in different symmetry operations."
1223
                )
1224
                self._structure.remove_site_property("magmom")
×
1225
            sga = SpacegroupAnalyzer(self._structure)
1✔
1226
            ops = sga.get_symmetry_operations()
1✔
1227
            isomorphic_point_group = [op.rotation_matrix for op in ops]
1✔
1228

1229
            recip_point_group = self._get_reciprocal_point_group(isomorphic_point_group, PAR, A)
1✔
1230

1231
        self._rpg = recip_point_group
1✔
1232

1233
        # 2: Get all vertices, edge- and face- center points of BZ ("key points")
1234

1235
        key_points, bz_as_key_point_inds, face_center_inds = self._get_key_points()
1✔
1236

1237
        # 3: Find symmetry-equivalent points, which can be mapped to each other by a combination of point group
1238
        # operations and integer translations by lattice vectors. The integers will only be -1, 0, or 1, since
1239
        # we are restricted to the BZ.
1240

1241
        key_points_inds_orbits = self._get_key_point_orbits(key_points=key_points)
1✔
1242

1243
        # 4: Get all lines in BZ between adjacent key points and between gamma
1244
        # and key points ("key lines")
1245

1246
        key_lines = self._get_key_lines(key_points=key_points, bz_as_key_point_inds=bz_as_key_point_inds)
1✔
1247

1248
        # 5: Find symmetry-equivalent key lines, defined as end points of first line being equivalent
1249
        # to end points of second line, and a random point in between being equivalent to the mapped
1250
        # random point.
1251

1252
        key_lines_inds_orbits = self._get_key_line_orbits(
1✔
1253
            key_points=key_points,
1254
            key_lines=key_lines,
1255
            key_points_inds_orbits=key_points_inds_orbits,
1256
        )
1257

1258
        # 6 & 7: Get little groups for key points (group of symmetry elements present at that point).
1259
        # Get little groups for key lines (group of symmetry elements present at every point
1260
        # along the line). This is implemented by testing the symmetry at a point e/pi of the
1261
        # way between the two endpoints.
1262

1263
        little_groups_points, little_groups_lines = self._get_little_groups(
1✔
1264
            key_points=key_points,
1265
            key_points_inds_orbits=key_points_inds_orbits,
1266
            key_lines_inds_orbits=key_lines_inds_orbits,
1267
        )
1268

1269
        # 8: Choose key lines for k-path. Loose criteria set: choose any points / segments
1270
        # with spatial symmetry greater than the general position (AKA more symmetry operations
1271
        # than just the identity or identity * TR in the little group).
1272
        # This function can be edited to alter high-symmetry criteria for choosing points and lines
1273

1274
        point_orbits_in_path, line_orbits_in_path = self._choose_path(
1✔
1275
            key_points=key_points,
1276
            key_points_inds_orbits=key_points_inds_orbits,
1277
            key_lines_inds_orbits=key_lines_inds_orbits,
1278
            little_groups_points=little_groups_points,
1279
            little_groups_lines=little_groups_lines,
1280
        )
1281

1282
        # 10: Consolidate selected segments into a single irreducible section of the BZ (as determined
1283
        # by the reciprocal point and lattice symmetries). This is accomplished by identifying the boundary
1284
        # planes of the IRBZ. Also, get labels for points according to distance from axes.
1285

1286
        IRBZ_points_inds = self._get_IRBZ(recip_point_group, W, key_points, face_center_inds, atol)
1✔
1287
        lines_in_path_inds = []
1✔
1288
        for ind in line_orbits_in_path:
1✔
1289
            for tup in key_lines_inds_orbits[ind]:
1✔
1290
                if tup[0] in IRBZ_points_inds and tup[1] in IRBZ_points_inds:
1✔
1291
                    lines_in_path_inds.append(tup)
1✔
1292
                    break
1✔
1293
        G = nx.Graph(lines_in_path_inds)
1✔
1294
        lines_in_path_inds = list(nx.edge_dfs(G))
1✔
1295
        points_in_path_inds = [ind for tup in lines_in_path_inds for ind in tup]
1✔
1296
        points_in_path_inds_unique = list(set(points_in_path_inds))
1✔
1297

1298
        orbit_cosines = []
1✔
1299
        for orbit in key_points_inds_orbits[:-1]:
1✔
1300
            orbit_cosines.append(
1✔
1301
                sorted(
1302
                    sorted(
1303
                        (
1304
                            (
1305
                                j,
1306
                                np.round(
1307
                                    np.dot(key_points[k], self.LabelPoints(j))
1308
                                    / (np.linalg.norm(key_points[k]) * np.linalg.norm(self.LabelPoints(j))),
1309
                                    decimals=3,
1310
                                ),
1311
                            )
1312
                            for k in orbit
1313
                            for j in range(26)
1314
                        ),
1315
                        key=operator.itemgetter(0),
1316
                    ),
1317
                    key=operator.itemgetter(1),
1318
                    reverse=True,
1319
                )
1320
            )
1321

1322
        orbit_labels = self._get_orbit_labels(orbit_cosines, key_points_inds_orbits, atol)
1✔
1323
        key_points_labels = ["" for i in range(len(key_points))]
1✔
1324
        for i, orbit in enumerate(key_points_inds_orbits):
1✔
1325
            for point_ind in orbit:
1✔
1326
                key_points_labels[point_ind] = self.LabelSymbol(int(orbit_labels[i]))
1✔
1327

1328
        kpoints = {}
1✔
1329
        reverse_kpoints = {}
1✔
1330
        for point_ind in points_in_path_inds_unique:
1✔
1331
            point_label = key_points_labels[point_ind]
1✔
1332
            if point_label not in kpoints:
1✔
1333
                kpoints[point_label] = key_points[point_ind]
1✔
1334
                reverse_kpoints[point_ind] = point_label
1✔
1335
            else:
1336
                existing_labels = [key for key in kpoints if point_label in key]
1✔
1337
                if "'" not in point_label:
1✔
1338
                    existing_labels[:] = [label for label in existing_labels if "'" not in label]
1✔
1339
                if len(existing_labels) == 1:
1✔
1340
                    max_occurence = 0
1✔
1341
                else:
1342
                    if "'" not in point_label:
1✔
1343
                        max_occurence = max(int(label[3:-1]) for label in existing_labels[1:])
1✔
1344
                    else:
1345
                        max_occurence = max(int(label[4:-1]) for label in existing_labels[1:])
×
1346
                kpoints[point_label + "_{" + str(max_occurence + 1) + "}"] = key_points[point_ind]
1✔
1347
                reverse_kpoints[point_ind] = point_label + "_{" + str(max_occurence + 1) + "}"
1✔
1348

1349
        path = []
1✔
1350
        i = 0
1✔
1351
        start_of_subpath = True
1✔
1352
        while i < len(points_in_path_inds):
1✔
1353
            if start_of_subpath:
1✔
1354
                path.append([reverse_kpoints[points_in_path_inds[i]]])
1✔
1355
                i += 1
1✔
1356
                start_of_subpath = False
1✔
1357
            elif points_in_path_inds[i] == points_in_path_inds[i + 1]:
1✔
1358
                path[-1].append(reverse_kpoints[points_in_path_inds[i]])
1✔
1359
                i += 2
1✔
1360
            else:
1361
                path[-1].append(reverse_kpoints[points_in_path_inds[i]])
1✔
1362
                i += 1
1✔
1363
                start_of_subpath = True
1✔
1364
            if i == len(points_in_path_inds) - 1:
1✔
1365
                path[-1].append(reverse_kpoints[points_in_path_inds[i]])
1✔
1366
                i += 1
1✔
1367

1368
        return {"kpoints": kpoints, "path": path}
1✔
1369

1370
    def _choose_path(
1✔
1371
        self,
1372
        key_points,
1373
        key_points_inds_orbits,
1374
        key_lines_inds_orbits,
1375
        little_groups_points,
1376
        little_groups_lines,
1377
    ):
1378
        #
1379
        # This function can be edited to alter high-symmetry criteria for choosing points and lines
1380
        #
1381

1382
        ID = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
1✔
1383
        PAR = np.array([[-1, 0, 0], [0, -1, 0], [0, 0, -1]])
1✔
1384

1385
        gamma_ind = len(key_points) - 1
1✔
1386

1387
        line_orbits_in_path = []
1✔
1388
        point_orbits_in_path = []
1✔
1389
        for i, little_group in enumerate(little_groups_lines):
1✔
1390
            add_rep = False
1✔
1391
            nC2 = 0
1✔
1392
            nC3 = 0
1✔
1393
            nsig = 0
1✔
1394
            for opind in little_group:
1✔
1395
                op = self._rpg[opind]
1✔
1396
                if not (op == ID).all():
1✔
1397
                    if (np.dot(op, op) == ID).all():
1✔
1398
                        if np.linalg.det(op) == 1:
1✔
1399
                            nC2 += 1
1✔
1400
                            break
1✔
1401
                        if not (op == PAR).all():
1✔
1402
                            nsig += 1
1✔
1403
                            break
1✔
1404
                    elif (np.dot(op, np.dot(op, op)) == ID).all():
1✔
1405
                        nC3 += 1
1✔
1406
                        break
1✔
1407
            if nC2 > 0 or nC3 > 0 or nsig > 0:
1✔
1408
                add_rep = True
1✔
1409

1410
            if add_rep:
1✔
1411
                line_orbits_in_path.append(i)
1✔
1412
                l = key_lines_inds_orbits[i][0]
1✔
1413
                ind0 = l[0]
1✔
1414
                ind1 = l[1]
1✔
1415
                found0 = False
1✔
1416
                found1 = False
1✔
1417
                for j, orbit in enumerate(key_points_inds_orbits):
1✔
1418
                    if ind0 in orbit:
1✔
1419
                        point_orbits_in_path.append(j)
1✔
1420
                        found0 = True
1✔
1421
                    if ind1 in orbit:
1✔
1422
                        point_orbits_in_path.append(j)
1✔
1423
                        found1 = True
1✔
1424
                    if found0 and found1:
1✔
1425
                        break
1✔
1426
        point_orbits_in_path = list(set(point_orbits_in_path))
1✔
1427

1428
        # Choose remaining unconnected key points for k-path. The ones that remain are
1429
        # those with inversion symmetry. Connect them to gamma.
1430

1431
        unconnected = []
1✔
1432

1433
        for i in range(len(key_points_inds_orbits)):
1✔
1434
            if i not in point_orbits_in_path:
1✔
1435
                unconnected.append(i)
1✔
1436

1437
        for ind in unconnected:
1✔
1438
            connect = False
1✔
1439
            for op_ind in little_groups_points[ind]:
1✔
1440
                op = self._rpg[op_ind]
1✔
1441
                if (op == ID).all():
1✔
1442
                    pass
1✔
1443
                elif (op == PAR).all():
1✔
1444
                    connect = True
1✔
1445
                    break
1✔
1446
                elif np.linalg.det(op) == 1:
×
1447
                    if (np.dot(op, np.dot(op, op)) == ID).all():
×
1448
                        pass
×
1449
                    else:
1450
                        connect = True
×
1451
                        break
×
1452
                else:
1453
                    pass
1✔
1454
            if connect:
1✔
1455
                l = (key_points_inds_orbits[ind][0], gamma_ind)
1✔
1456
                for j, orbit in enumerate(key_lines_inds_orbits):
1✔
1457
                    if l in orbit:
1✔
1458
                        line_orbits_in_path.append(j)
1✔
1459
                        break
1✔
1460
                if gamma_ind not in point_orbits_in_path:
1✔
1461
                    point_orbits_in_path.append(gamma_ind)
1✔
1462
                point_orbits_in_path.append(ind)
1✔
1463

1464
        return point_orbits_in_path, line_orbits_in_path
1✔
1465

1466
    def _get_key_points(self):
1✔
1467
        decimals = ceil(-1 * np.log10(self._atol)) - 1
1✔
1468
        bz = self._rec_lattice.get_wigner_seitz_cell()
1✔
1469

1470
        key_points = []
1✔
1471
        face_center_inds = []
1✔
1472
        bz_as_key_point_inds = []
1✔
1473

1474
        # pymatgen gives BZ in Cartesian coordinates; convert to fractional in
1475
        # the primitive basis for reciprocal space
1476
        for i, facet in enumerate(bz):
1✔
1477
            for j, vert in enumerate(facet):
1✔
1478
                vert = self._rec_lattice.get_fractional_coords(vert)
1✔
1479
                bz[i][j] = vert
1✔
1480
        pop = []
1✔
1481
        for i, facet in enumerate(bz):
1✔
1482
            rounded_facet = np.around(facet, decimals=decimals)
1✔
1483
            u, indices = np.unique(rounded_facet, axis=0, return_index=True)
1✔
1484
            if len(u) in [1, 2]:
1✔
1485
                pop.append(i)
×
1486
            else:
1487
                bz[i] = [facet[j] for j in np.sort(indices)]
1✔
1488
        bz = [bz[i] for i in range(len(bz)) if i not in pop]
1✔
1489

1490
        # use vertex points to calculate edge- and face- centers
1491
        for i, facet in enumerate(bz):
1✔
1492
            bz_as_key_point_inds.append([])
1✔
1493
            for j, vert in enumerate(facet):
1✔
1494
                edge_center = (vert + facet[j + 1]) / 2.0 if j != len(facet) - 1 else (vert + facet[0]) / 2.0
1✔
1495
                duplicatevert = False
1✔
1496
                duplicateedge = False
1✔
1497
                for k, point in enumerate(key_points):
1✔
1498
                    if np.allclose(vert, point, atol=self._atol):
1✔
1499
                        bz_as_key_point_inds[i].append(k)
1✔
1500
                        duplicatevert = True
1✔
1501
                        break
1✔
1502
                for k, point in enumerate(key_points):
1✔
1503
                    if np.allclose(edge_center, point, atol=self._atol):
1✔
1504
                        bz_as_key_point_inds[i].append(k)
1✔
1505
                        duplicateedge = True
1✔
1506
                        break
1✔
1507
                if not duplicatevert:
1✔
1508
                    key_points.append(vert)
1✔
1509
                    bz_as_key_point_inds[i].append(len(key_points) - 1)
1✔
1510
                if not duplicateedge:
1✔
1511
                    key_points.append(edge_center)
1✔
1512
                    bz_as_key_point_inds[i].append(len(key_points) - 1)
1✔
1513
            if len(facet) == 4:  # parallelogram facet
1✔
1514
                face_center = (facet[0] + facet[1] + facet[2] + facet[3]) / 4.0
1✔
1515
                key_points.append(face_center)
1✔
1516
                face_center_inds.append(len(key_points) - 1)
1✔
1517
                bz_as_key_point_inds[i].append(len(key_points) - 1)
1✔
1518
            else:  # hexagonal facet
1519
                face_center = (facet[0] + facet[1] + facet[2] + facet[3] + facet[4] + facet[5]) / 6.0
1✔
1520
                key_points.append(face_center)
1✔
1521
                face_center_inds.append(len(key_points) - 1)
1✔
1522
                bz_as_key_point_inds[i].append(len(key_points) - 1)
1✔
1523

1524
        # add gamma point
1525
        key_points.append(np.array([0, 0, 0]))
1✔
1526
        return key_points, bz_as_key_point_inds, face_center_inds
1✔
1527

1528
    def _get_key_point_orbits(self, key_points):
1✔
1529
        key_points_copy = dict(zip(range(len(key_points) - 1), key_points[0 : len(key_points) - 1]))
1✔
1530
        # gamma not equivalent to any in BZ and is last point added to
1531
        # key_points
1532
        key_points_inds_orbits = []
1✔
1533

1534
        i = 0
1✔
1535
        while len(key_points_copy) > 0:
1✔
1536
            key_points_inds_orbits.append([])
1✔
1537
            k0ind = list(key_points_copy)[0]
1✔
1538
            k0 = key_points_copy[k0ind]
1✔
1539
            key_points_inds_orbits[i].append(k0ind)
1✔
1540
            key_points_copy.pop(k0ind)
1✔
1541

1542
            for op in self._rpg:
1✔
1543
                to_pop = []
1✔
1544
                k1 = np.dot(op, k0)
1✔
1545
                for ind_key in key_points_copy:
1✔
1546
                    diff = k1 - key_points_copy[ind_key]
1✔
1547
                    if self._all_ints(diff, atol=self._atol):
1✔
1548
                        key_points_inds_orbits[i].append(ind_key)
1✔
1549
                        to_pop.append(ind_key)
1✔
1550

1551
                for key in to_pop:
1✔
1552
                    key_points_copy.pop(key)
1✔
1553
            i += 1
1✔
1554

1555
        key_points_inds_orbits.append([len(key_points) - 1])
1✔
1556

1557
        return key_points_inds_orbits
1✔
1558

1559
    @staticmethod
1✔
1560
    def _get_key_lines(key_points, bz_as_key_point_inds):
1✔
1561
        key_lines = []
1✔
1562
        gamma_ind = len(key_points) - 1
1✔
1563

1564
        for facet_as_key_point_inds in bz_as_key_point_inds:
1✔
1565
            facet_as_key_point_inds_bndy = facet_as_key_point_inds[: len(facet_as_key_point_inds) - 1]
1✔
1566
            # not the face center point (don't need to check it since it's not
1567
            # shared with other facets)
1568
            face_center_ind = facet_as_key_point_inds[-1]
1✔
1569
            for j, ind in enumerate(facet_as_key_point_inds_bndy):
1✔
1570
                if (
1✔
1571
                    min(ind, facet_as_key_point_inds_bndy[j - 1]),
1572
                    max(ind, facet_as_key_point_inds_bndy[j - 1]),
1573
                ) not in key_lines:
1574
                    key_lines.append(
1✔
1575
                        (
1576
                            min(ind, facet_as_key_point_inds_bndy[j - 1]),
1577
                            max(ind, facet_as_key_point_inds_bndy[j - 1]),
1578
                        )
1579
                    )
1580
                k = j + 1 if j != len(facet_as_key_point_inds_bndy) - 1 else 0
1✔
1581
                if (
1✔
1582
                    min(ind, facet_as_key_point_inds_bndy[k]),
1583
                    max(ind, facet_as_key_point_inds_bndy[k]),
1584
                ) not in key_lines:
1585
                    key_lines.append(
1✔
1586
                        (
1587
                            min(ind, facet_as_key_point_inds_bndy[k]),
1588
                            max(ind, facet_as_key_point_inds_bndy[k]),
1589
                        )
1590
                    )
1591
                if (ind, gamma_ind) not in key_lines:
1✔
1592
                    key_lines.append((ind, gamma_ind))
1✔
1593
                key_lines.append((min(ind, face_center_ind), max(ind, face_center_ind)))
1✔
1594
            key_lines.append((face_center_ind, gamma_ind))
1✔
1595

1596
        return key_lines
1✔
1597

1598
    def _get_key_line_orbits(self, key_points, key_lines, key_points_inds_orbits):
1✔
1599
        key_lines_copy = dict(zip(range(len(key_lines)), key_lines))
1✔
1600
        key_lines_inds_orbits = []
1✔
1601

1602
        i = 0
1✔
1603
        while len(key_lines_copy) > 0:
1✔
1604
            key_lines_inds_orbits.append([])
1✔
1605
            l0ind = list(key_lines_copy)[0]
1✔
1606
            l0 = key_lines_copy[l0ind]
1✔
1607
            key_lines_inds_orbits[i].append(l0)
1✔
1608
            key_lines_copy.pop(l0ind)
1✔
1609
            to_pop = []
1✔
1610
            p00 = key_points[l0[0]]
1✔
1611
            p01 = key_points[l0[1]]
1✔
1612
            pmid0 = p00 + e / pi * (p01 - p00)
1✔
1613
            for ind_key in key_lines_copy:
1✔
1614
                l1 = key_lines_copy[ind_key]
1✔
1615
                p10 = key_points[l1[0]]
1✔
1616
                p11 = key_points[l1[1]]
1✔
1617
                equivptspar = False
1✔
1618
                equivptsperp = False
1✔
1619
                equivline = False
1✔
1620

1621
                if (
1✔
1622
                    np.array([l0[0] in orbit and l1[0] in orbit for orbit in key_points_inds_orbits]).any()
1623
                    and np.array([l0[1] in orbit and l1[1] in orbit for orbit in key_points_inds_orbits]).any()
1624
                ):
1625
                    equivptspar = True
1✔
1626
                elif (
1✔
1627
                    np.array([l0[1] in orbit and l1[0] in orbit for orbit in key_points_inds_orbits]).any()
1628
                    and np.array([l0[0] in orbit and l1[1] in orbit for orbit in key_points_inds_orbits]).any()
1629
                ):
1630
                    equivptsperp = True
1✔
1631

1632
                if equivptspar:
1✔
1633
                    pmid1 = p10 + e / pi * (p11 - p10)
1✔
1634
                    for op in self._rpg:
1✔
1635
                        if not equivline:
1✔
1636
                            p00pr = np.dot(op, p00)
1✔
1637
                            diff0 = p10 - p00pr
1✔
1638
                            if self._all_ints(diff0, atol=self._atol):
1✔
1639
                                pmid0pr = np.dot(op, pmid0) + diff0
1✔
1640
                                p01pr = np.dot(op, p01) + diff0
1✔
1641
                                if np.allclose(p11, p01pr, atol=self._atol) and np.allclose(
1✔
1642
                                    pmid1, pmid0pr, atol=self._atol
1643
                                ):
1644
                                    equivline = True
1✔
1645

1646
                elif equivptsperp:
1✔
1647
                    pmid1 = p11 + e / pi * (p10 - p11)
1✔
1648
                    for op in self._rpg:
1✔
1649
                        if not equivline:
1✔
1650
                            p00pr = np.dot(op, p00)
1✔
1651
                            diff0 = p11 - p00pr
1✔
1652
                            if self._all_ints(diff0, atol=self._atol):
1✔
1653
                                pmid0pr = np.dot(op, pmid0) + diff0
1✔
1654
                                p01pr = np.dot(op, p01) + diff0
1✔
1655
                                if np.allclose(p10, p01pr, atol=self._atol) and np.allclose(
1✔
1656
                                    pmid1, pmid0pr, atol=self._atol
1657
                                ):
1658
                                    equivline = True
1✔
1659

1660
                if equivline:
1✔
1661
                    key_lines_inds_orbits[i].append(l1)
1✔
1662
                    to_pop.append(ind_key)
1✔
1663

1664
            for key in to_pop:
1✔
1665
                key_lines_copy.pop(key)
1✔
1666
            i += 1
1✔
1667

1668
        return key_lines_inds_orbits
1✔
1669

1670
    def _get_little_groups(self, key_points, key_points_inds_orbits, key_lines_inds_orbits):
1✔
1671
        little_groups_points = []  # elements are lists of indices of recip_point_group. the
1✔
1672
        # list little_groups_points[i] is the little group for the
1673
        # orbit key_points_inds_orbits[i]
1674
        for i, orbit in enumerate(key_points_inds_orbits):
1✔
1675
            k0 = key_points[orbit[0]]
1✔
1676
            little_groups_points.append([])
1✔
1677
            for j, op in enumerate(self._rpg):
1✔
1678
                gamma_to = np.dot(op, -1 * k0) + k0
1✔
1679
                check_gamma = True
1✔
1680
                if not self._all_ints(gamma_to, atol=self._atol):
1✔
1681
                    check_gamma = False
1✔
1682
                if check_gamma:
1✔
1683
                    little_groups_points[i].append(j)
1✔
1684

1685
        # elements are lists of indices of recip_point_group. the list
1686
        # little_groups_lines[i] is
1687
        little_groups_lines = []
1✔
1688
        # the little group for the orbit key_points_inds_lines[i]
1689

1690
        for i, orbit in enumerate(key_lines_inds_orbits):
1✔
1691
            l0 = orbit[0]
1✔
1692
            v = key_points[l0[1]] - key_points[l0[0]]
1✔
1693
            k0 = key_points[l0[0]] + np.e / pi * v
1✔
1694
            little_groups_lines.append([])
1✔
1695
            for j, op in enumerate(self._rpg):
1✔
1696
                gamma_to = np.dot(op, -1 * k0) + k0
1✔
1697
                check_gamma = True
1✔
1698
                if not self._all_ints(gamma_to, atol=self._atol):
1✔
1699
                    check_gamma = False
1✔
1700
                if check_gamma:
1✔
1701
                    little_groups_lines[i].append(j)
1✔
1702

1703
        return little_groups_points, little_groups_lines
1✔
1704

1705
    def _convert_all_magmoms_to_vectors(self, magmom_axis, axis_specified):
1✔
1706
        struct = self._structure.copy()
1✔
1707
        magmom_axis = np.array(magmom_axis)
1✔
1708
        if "magmom" not in struct.site_properties:
1✔
1709
            warn(
×
1710
                "The 'magmom' property is not set in the structure's site properties."
1711
                "All magnetic moments are being set to zero."
1712
            )
1713
            struct.add_site_property("magmom", [np.array([0, 0, 0]) for i in range(len(struct.sites))])
×
1714

1715
            return struct
×
1716

1717
        old_magmoms = struct.site_properties["magmom"]
1✔
1718
        new_magmoms = []
1✔
1719
        found_scalar = False
1✔
1720

1721
        for magmom in old_magmoms:
1✔
1722
            if isinstance(magmom, np.ndarray):
1✔
1723
                new_magmoms.append(magmom)
1✔
1724
            elif isinstance(magmom, list):
×
1725
                new_magmoms.append(np.array(magmom))
×
1726
            else:
1727
                found_scalar = True
×
1728
                new_magmoms.append(magmom * magmom_axis)
×
1729

1730
        if found_scalar and not axis_specified:
1✔
1731
            warn("At least one magmom had a scalar value and magmom_axis was not specified. Defaulted to z+ spinor.")
×
1732

1733
        struct.remove_site_property("magmom")
1✔
1734
        struct.add_site_property("magmom", new_magmoms)
1✔
1735
        return struct
1✔
1736

1737
    def _get_magnetic_symmetry_operations(self, struct, grey_ops, atol):
1✔
1738
        mag_ops = []
1✔
1739
        magmoms = struct.site_properties["magmom"]
1✔
1740
        nonzero_magmom_inds = [i for i in range(len(struct.sites)) if not (magmoms[i] == np.array([0, 0, 0])).all()]
1✔
1741
        init_magmoms = [site.properties["magmom"] for (i, site) in enumerate(struct.sites) if i in nonzero_magmom_inds]
1✔
1742
        sites = [site for (i, site) in enumerate(struct.sites) if i in nonzero_magmom_inds]
1✔
1743
        init_site_coords = [site.frac_coords for site in sites]
1✔
1744
        for op in grey_ops:
1✔
1745
            r = op.rotation_matrix
1✔
1746
            t = op.translation_vector
1✔
1747
            xformed_magmoms = [self._apply_op_to_magmom(r, magmom) for magmom in init_magmoms]
1✔
1748
            xformed_site_coords = [np.dot(r, site.frac_coords) + t for site in sites]
1✔
1749
            permutation = ["a" for i in range(len(sites))]
1✔
1750
            not_found = list(range(len(sites)))
1✔
1751
            for i in range(len(sites)):
1✔
1752
                xformed = xformed_site_coords[i]
1✔
1753
                for k, j in enumerate(not_found):
1✔
1754
                    init = init_site_coords[j]
1✔
1755
                    diff = xformed - init
1✔
1756
                    if self._all_ints(diff, atol=atol):
1✔
1757
                        permutation[i] = j
1✔
1758
                        not_found.pop(k)
1✔
1759
                        break
1✔
1760

1761
            same = np.zeros(len(sites))
1✔
1762
            flipped = np.zeros(len(sites))
1✔
1763
            for i, magmom in enumerate(xformed_magmoms):
1✔
1764
                if (magmom == init_magmoms[permutation[i]]).all():
1✔
1765
                    same[i] = 1
1✔
1766
                elif (magmom == -1 * init_magmoms[permutation[i]]).all():
1✔
1767
                    flipped[i] = 1
1✔
1768

1769
            if same.all():  # add symm op without tr
1✔
1770
                mag_ops.append(
1✔
1771
                    MagSymmOp.from_rotation_and_translation_and_time_reversal(
1772
                        rotation_matrix=op.rotation_matrix,
1773
                        translation_vec=op.translation_vector,
1774
                        time_reversal=1,
1775
                    )
1776
                )
1777
            if flipped.all():  # add symm op with tr
1✔
1778
                mag_ops.append(
1✔
1779
                    MagSymmOp.from_rotation_and_translation_and_time_reversal(
1780
                        rotation_matrix=op.rotation_matrix,
1781
                        translation_vec=op.translation_vector,
1782
                        time_reversal=-1,
1783
                    )
1784
                )
1785

1786
        return mag_ops
1✔
1787

1788
    @staticmethod
1✔
1789
    def _get_reciprocal_point_group(ops, R, A):
1✔
1790
        Ainv = np.linalg.inv(A)
1✔
1791
        # convert to reciprocal primitive basis
1792
        recip_point_group = [np.around(np.dot(A, np.dot(R, Ainv)), decimals=2)]
1✔
1793
        for op in ops:
1✔
1794
            op = np.around(np.dot(A, np.dot(op, Ainv)), decimals=2)
1✔
1795
            new = True
1✔
1796
            new_coset = True
1✔
1797
            for thing in recip_point_group:
1✔
1798
                if (thing == op).all():
1✔
1799
                    new = False
1✔
1800
                if (thing == np.dot(R, op)).all():
1✔
1801
                    new_coset = False
1✔
1802

1803
            if new:
1✔
1804
                recip_point_group.append(op)
1✔
1805
            if new_coset:
1✔
1806
                recip_point_group.append(np.dot(R, op))
1✔
1807

1808
        return recip_point_group
1✔
1809

1810
    @staticmethod
1✔
1811
    def _closewrapped(pos1, pos2, tolerance):
1✔
1812
        pos1 = pos1 % 1.0
1✔
1813
        pos2 = pos2 % 1.0
1✔
1814

1815
        if len(pos1) != len(pos2):
1✔
1816
            return False
×
1817
        for idx, p1 in enumerate(pos1):
1✔
1818
            if abs(p1 - pos2[idx]) > tolerance[idx] and abs(p1 - pos2[idx]) < 1.0 - tolerance[idx]:
1✔
1819
                return False
×
1820
        return True
1✔
1821

1822
    def _get_coset_factor(self, G, H):
1✔
1823
        # finds g for left coset decomposition G = H + gH (H must be subgroup of G with index two.)
1824
        # in this implementation, G and H are lists of objects of type
1825
        # SymmOp
1826
        gH = []
1✔
1827
        for op1 in G:
1✔
1828
            in_H = False
1✔
1829
            for op2 in H:
1✔
1830
                if np.allclose(op1.rotation_matrix, op2.rotation_matrix, atol=self._atol) and self._closewrapped(
1✔
1831
                    op1.translation_vector,
1832
                    op2.translation_vector,
1833
                    np.ones(3) * self._atol,
1834
                ):
1835
                    in_H = True
1✔
1836
                    break
1✔
1837
            if not in_H:
1✔
1838
                gH.append(op1)
1✔
1839

1840
        for op in gH:
1✔
1841
            opH = [op * h for h in H]
1✔
1842
            is_coset_factor = True
1✔
1843
            for op1 in opH:
1✔
1844
                for op2 in H:
1✔
1845
                    if np.allclose(op1.rotation_matrix, op2.rotation_matrix, atol=self._atol) and self._closewrapped(
1✔
1846
                        op1.translation_vector,
1847
                        op2.translation_vector,
1848
                        np.ones(3) * self._atol,
1849
                    ):
1850
                        is_coset_factor = False
×
1851
                        break
×
1852
                if not is_coset_factor:
1✔
1853
                    break
×
1854
            if is_coset_factor:
1✔
1855
                return op
1✔
1856

1857
        return "No coset factor found."
×
1858

1859
    @staticmethod
1✔
1860
    def _apply_op_to_magmom(r, magmom):
1✔
1861
        if np.linalg.det(r) == 1:
1✔
1862
            return np.dot(r, magmom)
1✔
1863
        return -1 * np.dot(r, magmom)
1✔
1864

1865
    @staticmethod
1✔
1866
    def _all_ints(arr, atol):
1✔
1867
        rounded_arr = np.around(arr, decimals=0)
1✔
1868
        return np.allclose(rounded_arr, arr, atol=atol)
1✔
1869

1870
    def _get_IRBZ(self, recip_point_group, W, key_points, face_center_inds, atol):
1✔
1871
        rpgdict = self._get_reciprocal_point_group_dict(recip_point_group, atol)
1✔
1872

1873
        g = np.dot(W.T, W)  # just using change of basis matrix rather than
1✔
1874
        # Lattice.get_cartesian_coordinates for conciseness
1875
        ginv = np.linalg.inv(g)
1✔
1876
        D = np.linalg.det(W)
1✔
1877

1878
        primary_orientation = None
1✔
1879
        secondary_orientation = None
1✔
1880
        tertiary_orientation = None
1✔
1881

1882
        planar_boundaries = []
1✔
1883
        IRBZ_points = list(enumerate(key_points))
1✔
1884

1885
        for sigma in rpgdict["reflections"]:
1✔
1886
            norm = sigma["normal"]
1✔
1887
            if primary_orientation is None:
1✔
1888
                primary_orientation = norm
1✔
1889
                planar_boundaries.append(norm)
1✔
1890
            elif np.isclose(np.dot(primary_orientation, np.dot(g, norm)), 0, atol=atol):
1✔
1891
                if secondary_orientation is None:
1✔
1892
                    secondary_orientation = norm
1✔
1893
                    planar_boundaries.append(norm)
1✔
1894
                elif np.isclose(np.dot(secondary_orientation, np.dot(g, norm)), 0, atol=atol):
1✔
1895
                    if tertiary_orientation is None:
1✔
1896
                        tertiary_orientation = norm
1✔
1897
                        planar_boundaries.append(norm)
1✔
1898
                    elif np.allclose(norm, -1 * tertiary_orientation, atol=atol):
×
1899
                        pass
1✔
1900
                elif np.dot(secondary_orientation, np.dot(g, norm)) < 0:
1✔
1901
                    planar_boundaries.append(-1 * norm)
1✔
1902
                else:
1903
                    planar_boundaries.append(norm)
1✔
1904
            elif np.dot(primary_orientation, np.dot(g, norm)) < 0:
1✔
1905
                planar_boundaries.append(-1 * norm)
1✔
1906
            else:
1907
                planar_boundaries.append(norm)
1✔
1908

1909
        IRBZ_points = self._reduce_IRBZ(IRBZ_points, planar_boundaries, g, atol)
1✔
1910

1911
        used_axes = []
1✔
1912

1913
        # six-fold rotoinversion always comes with horizontal mirror so don't
1914
        # need to check
1915
        for rotn in rpgdict["rotations"]["six-fold"]:
1✔
1916
            ax = rotn["axis"]
1✔
1917
            op = rotn["op"]
1✔
1918
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
1919
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
1920
                    face_center_found = False
1✔
1921
                    for point in IRBZ_points:
1✔
1922
                        if point[0] in face_center_inds:
1✔
1923
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
1✔
1924
                            if not np.allclose(cross, 0, atol=atol):
1✔
1925
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
1✔
1926
                                face_center_found = True
1✔
1927
                                used_axes.append(ax)
1✔
1928
                                break
1✔
1929
                    if not face_center_found:
1✔
1930
                        print("face center not found")
×
1931
                        for point in IRBZ_points:
×
1932
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
1933
                            if not np.allclose(cross, 0, atol=atol):
×
1934
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
1935
                                used_axes.append(ax)
×
1936
                                break
×
1937
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
1✔
1938

1939
        for rotn in rpgdict["rotations"]["rotoinv-four-fold"]:
1✔
1940
            ax = rotn["axis"]
1✔
1941
            op = rotn["op"]
1✔
1942
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
1943
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
1944
                    face_center_found = False
×
1945
                    for point in IRBZ_points:
×
1946
                        if point[0] in face_center_inds:
×
1947
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
1948
                            if not np.allclose(cross, 0, atol=atol):
×
1949
                                rot_boundaries = [cross, np.dot(op, cross)]
×
1950
                                face_center_found = True
×
1951
                                used_axes.append(ax)
×
1952
                                break
×
1953
                    if not face_center_found:
×
1954
                        print("face center not found")
×
1955
                        for point in IRBZ_points:
×
1956
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
1957
                            if not np.allclose(cross, 0, atol=atol):
×
1958
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
1959
                                used_axes.append(ax)
×
1960
                                break
×
1961
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
×
1962

1963
        for rotn in rpgdict["rotations"]["four-fold"]:
1✔
1964
            ax = rotn["axis"]
1✔
1965
            op = rotn["op"]
1✔
1966
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
1967
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
1968
                    face_center_found = False
1✔
1969
                    for point in IRBZ_points:
1✔
1970
                        if point[0] in face_center_inds:
1✔
1971
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
1✔
1972
                            if not np.allclose(cross, 0, atol=atol):
1✔
1973
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
1✔
1974
                                face_center_found = True
1✔
1975
                                used_axes.append(ax)
1✔
1976
                                break
1✔
1977
                    if not face_center_found:
1✔
1978
                        print("face center not found")
×
1979
                        for point in IRBZ_points:
×
1980
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
1981
                            if not np.allclose(cross, 0, atol=atol):
×
1982
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
1983
                                used_axes.append(ax)
×
1984
                                break
×
1985
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
1✔
1986

1987
        for rotn in rpgdict["rotations"]["rotoinv-three-fold"]:
1✔
1988
            ax = rotn["axis"]
1✔
1989
            op = rotn["op"]
1✔
1990
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
1991
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
1992
                    face_center_found = False
1✔
1993
                    for point in IRBZ_points:
1✔
1994
                        if point[0] in face_center_inds:
1✔
1995
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
1✔
1996
                            if not np.allclose(cross, 0, atol=atol):
1✔
1997
                                rot_boundaries = [
1✔
1998
                                    cross,
1999
                                    -1 * np.dot(sqrtm(-1 * op), cross),
2000
                                ]
2001
                                face_center_found = True
1✔
2002
                                used_axes.append(ax)
1✔
2003
                                break
1✔
2004
                    if not face_center_found:
1✔
2005
                        print("face center not found")
×
2006
                        for point in IRBZ_points:
×
2007
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
2008
                            if not np.allclose(cross, 0, atol=atol):
×
2009
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
2010
                                used_axes.append(ax)
×
2011
                                break
×
2012
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
1✔
2013

2014
        for rotn in rpgdict["rotations"]["three-fold"]:
1✔
2015
            ax = rotn["axis"]
1✔
2016
            op = rotn["op"]
1✔
2017
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
2018
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
2019
                    face_center_found = False
1✔
2020
                    for point in IRBZ_points:
1✔
2021
                        if point[0] in face_center_inds:
1✔
2022
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
1✔
2023
                            if not np.allclose(cross, 0, atol=atol):
1✔
2024
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
1✔
2025
                                face_center_found = True
1✔
2026
                                used_axes.append(ax)
1✔
2027
                                break
1✔
2028
                    if not face_center_found:
1✔
2029
                        print("face center not found")
×
2030
                        for point in IRBZ_points:
×
2031
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
2032
                            if not np.allclose(cross, 0, atol=atol):
×
2033
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
2034
                                used_axes.append(ax)
×
2035
                                break
×
2036
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
1✔
2037

2038
        for rotn in rpgdict["rotations"]["two-fold"]:
1✔
2039
            ax = rotn["axis"]
1✔
2040
            op = rotn["op"]
1✔
2041
            if not np.any([np.allclose(ax, usedax, atol) for usedax in used_axes]):
1✔
2042
                if self._op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
2043
                    face_center_found = False
1✔
2044
                    for point in IRBZ_points:
1✔
2045
                        if point[0] in face_center_inds:
1✔
2046
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
1✔
2047
                            if not np.allclose(cross, 0, atol=atol):
1✔
2048
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
1✔
2049
                                face_center_found = True
1✔
2050
                                used_axes.append(ax)
1✔
2051
                                break
1✔
2052
                    if not face_center_found:
1✔
2053
                        print("face center not found")
×
2054
                        for point in IRBZ_points:
×
2055
                            cross = D * np.dot(ginv, np.cross(ax, point[1]))
×
2056
                            if not np.allclose(cross, 0, atol=atol):
×
2057
                                rot_boundaries = [cross, -1 * np.dot(op, cross)]
×
2058
                                used_axes.append(ax)
×
2059
                                break
×
2060
                    IRBZ_points = self._reduce_IRBZ(IRBZ_points, rot_boundaries, g, atol)
1✔
2061

2062
        return [point[0] for point in IRBZ_points]
1✔
2063

2064
    @staticmethod
1✔
2065
    def _get_reciprocal_point_group_dict(recip_point_group, atol):
1✔
2066
        PAR = np.array([[-1, 0, 0], [0, -1, 0], [0, 0, -1]])
1✔
2067

2068
        d = {
1✔
2069
            "reflections": [],
2070
            "rotations": {
2071
                "two-fold": [],
2072
                "three-fold": [],
2073
                "four-fold": [],
2074
                "six-fold": [],
2075
                "rotoinv-three-fold": [],
2076
                "rotoinv-four-fold": [],
2077
                "rotoinv-six-fold": [],
2078
            },
2079
            "inversion": [],
2080
        }
2081

2082
        for i, op in enumerate(recip_point_group):
1✔
2083
            evals, evects = np.linalg.eig(op)
1✔
2084

2085
            tr = np.trace(op)
1✔
2086
            det = np.linalg.det(op)
1✔
2087

2088
            # Proper rotations
2089
            if np.isclose(det, 1, atol=atol):
1✔
2090
                if np.isclose(tr, 3, atol=atol):
1✔
2091
                    continue
1✔
2092
                if np.isclose(tr, -1, atol=atol):  # two-fold rotation
1✔
2093
                    for j in range(3):
1✔
2094
                        if np.isclose(evals[j], 1, atol=atol):
1✔
2095
                            ax = evects[:, j]
1✔
2096
                    d["rotations"]["two-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2097
                elif np.isclose(tr, 0, atol=atol):  # three-fold rotation
1✔
2098
                    for j in range(3):
1✔
2099
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2100
                            ax = evects[:, j]
1✔
2101
                    d["rotations"]["three-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2102
                # four-fold rotation
2103
                elif np.isclose(tr, 1, atol=atol):
1✔
2104
                    for j in range(3):
1✔
2105
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2106
                            ax = evects[:, j]
1✔
2107
                    d["rotations"]["four-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2108
                elif np.isclose(tr, 2, atol=atol):  # six-fold rotation
1✔
2109
                    for j in range(3):
1✔
2110
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2111
                            ax = evects[:, j]
1✔
2112
                    d["rotations"]["six-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2113

2114
            # Improper rotations
2115
            if np.isclose(det, -1, atol=atol):
1✔
2116
                if np.isclose(tr, -3, atol=atol):
1✔
2117
                    d["inversion"].append({"ind": i, "op": PAR})
1✔
2118
                elif np.isclose(tr, 1, atol=atol):  # two-fold rotation
1✔
2119
                    for j in range(3):
1✔
2120
                        if np.isclose(evals[j], -1, atol=atol):
1✔
2121
                            norm = evects[:, j]
1✔
2122
                    d["reflections"].append({"ind": i, "normal": norm, "op": op})
1✔
2123
                elif np.isclose(tr, 0, atol=atol):  # three-fold rotoinversion
1✔
2124
                    for j in range(3):
1✔
2125
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2126
                            ax = evects[:, j]
1✔
2127
                    d["rotations"]["rotoinv-three-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2128
                # four-fold rotoinversion
2129
                elif np.isclose(tr, -1, atol=atol):
1✔
2130
                    for j in range(3):
1✔
2131
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2132
                            ax = evects[:, j]
1✔
2133
                    d["rotations"]["rotoinv-four-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2134
                # six-fold rotoinversion
2135
                elif np.isclose(tr, -2, atol=atol):
1✔
2136
                    for j in range(3):
1✔
2137
                        if np.isreal(evals[j]) and np.isclose(np.absolute(evals[j]), 1, atol=atol):
1✔
2138
                            ax = evects[:, j]
1✔
2139
                    d["rotations"]["rotoinv-six-fold"].append({"ind": i, "axis": ax, "op": op})
1✔
2140

2141
        return d
1✔
2142

2143
    @staticmethod
1✔
2144
    def _op_maps_IRBZ_to_self(op, IRBZ_points, atol):
1✔
2145
        point_coords = [point[1] for point in IRBZ_points]
1✔
2146
        for point in point_coords:
1✔
2147
            point_prime = np.dot(op, point)
1✔
2148
            mapped_back = False
1✔
2149
            for checkpoint in point_coords:
1✔
2150
                if np.allclose(point_prime, checkpoint, atol):
1✔
2151
                    mapped_back = True
1✔
2152
                    break
1✔
2153
            if not mapped_back:
1✔
2154
                return False
1✔
2155

2156
        return True
1✔
2157

2158
    @staticmethod
1✔
2159
    def _reduce_IRBZ(IRBZ_points, boundaries, g, atol):
1✔
2160
        in_reduced_section = []
1✔
2161
        for point in IRBZ_points:
1✔
2162
            in_reduced_section.append(
1✔
2163
                np.all(
2164
                    [
2165
                        (
2166
                            np.dot(point[1], np.dot(g, boundary)) >= 0
2167
                            or np.isclose(np.dot(point[1], np.dot(g, boundary)), 0, atol=atol)
2168
                        )
2169
                        for boundary in boundaries
2170
                    ]
2171
                )
2172
            )
2173

2174
        return [IRBZ_points[i] for i in range(len(IRBZ_points)) if in_reduced_section[i]]
1✔
2175

2176
    def _get_orbit_labels(self, orbit_cosines_orig, key_points_inds_orbits, atol):
1✔
2177
        orbit_cosines_copy = orbit_cosines_orig.copy()
1✔
2178
        orbit_labels_unsorted = [(len(key_points_inds_orbits) - 1, 26)]
1✔
2179
        orbit_inds_remaining = range(len(key_points_inds_orbits) - 1)
1✔
2180
        pop_orbits = []
1✔
2181
        pop_labels = []
1✔
2182

2183
        for i, orb_cos in enumerate(orbit_cosines_copy):
1✔
2184
            if np.isclose(orb_cos[0][1], 1.0, atol=atol):
1✔
2185
                # (point orbit index, label index)
2186
                orbit_labels_unsorted.append((i, orb_cos[0][0]))
1✔
2187
                pop_orbits.append(i)
1✔
2188
                pop_labels.append(orb_cos[0][0])
1✔
2189

2190
        orbit_cosines_copy = self._reduce_cosines_array(orbit_cosines_copy, pop_orbits, pop_labels)
1✔
2191
        orbit_inds_remaining = [i for i in orbit_inds_remaining if i not in pop_orbits]
1✔
2192

2193
        # orbit_labels_unsorted already contains gamma orbit
2194
        while len(orbit_labels_unsorted) < len(orbit_cosines_orig) + 1:
1✔
2195
            pop_orbits = []
1✔
2196
            pop_labels = []
1✔
2197
            max_cosine_value = max(orb_cos[0][1] for orb_cos in orbit_cosines_copy)
1✔
2198
            max_cosine_value_inds = [
1✔
2199
                j for j in range(len(orbit_cosines_copy)) if orbit_cosines_copy[j][0][1] == max_cosine_value
2200
            ]
2201
            max_cosine_label_inds = self._get_max_cosine_labels(
1✔
2202
                [orbit_cosines_copy[j] for j in max_cosine_value_inds],
2203
                key_points_inds_orbits,
2204
                atol,
2205
            )
2206

2207
            for j, label_ind in enumerate(max_cosine_label_inds):
1✔
2208
                orbit_labels_unsorted.append((orbit_inds_remaining[max_cosine_value_inds[j]], label_ind))
1✔
2209
                pop_orbits.append(max_cosine_value_inds[j])
1✔
2210
                pop_labels.append(label_ind)
1✔
2211
            orbit_cosines_copy = self._reduce_cosines_array(orbit_cosines_copy, pop_orbits, pop_labels)
1✔
2212
            orbit_inds_remaining = [
1✔
2213
                orbit_inds_remaining[j] for j in range(len(orbit_inds_remaining)) if j not in pop_orbits
2214
            ]
2215

2216
        orbit_labels = np.zeros(len(key_points_inds_orbits))
1✔
2217
        for tup in orbit_labels_unsorted:
1✔
2218
            orbit_labels[tup[0]] = tup[1]
1✔
2219

2220
        return orbit_labels
1✔
2221

2222
    @staticmethod
1✔
2223
    def _reduce_cosines_array(orbit_cosines, pop_orbits, pop_labels):
1✔
2224
        return [
1✔
2225
            [orb_cos[i] for i in range(len(orb_cos)) if orb_cos[i][0] not in pop_labels]
2226
            for j, orb_cos in enumerate(orbit_cosines)
2227
            if j not in pop_orbits
2228
        ]
2229

2230
    def _get_max_cosine_labels(self, max_cosine_orbits_orig, key_points_inds_orbits, atol):
1✔
2231
        max_cosine_orbits_copy = max_cosine_orbits_orig.copy()
1✔
2232
        max_cosine_label_inds = np.zeros(len(max_cosine_orbits_copy))
1✔
2233
        initial_max_cosine_label_inds = [max_cos_orb[0][0] for max_cos_orb in max_cosine_orbits_copy]
1✔
2234
        u, inds, counts = np.unique(initial_max_cosine_label_inds, return_index=True, return_counts=True)
1✔
2235
        grouped_inds = [
1✔
2236
            [
2237
                i
2238
                for i in range(len(initial_max_cosine_label_inds))
2239
                if max_cosine_orbits_copy[i][0][0] == max_cosine_orbits_copy[ind][0][0]
2240
            ]
2241
            for ind in inds
2242
        ]
2243
        pop_orbits = []
1✔
2244
        pop_labels = []
1✔
2245
        unassigned_orbits = []
1✔
2246
        for i, ind in enumerate(inds):
1✔
2247
            if counts[i] == 1:
1✔
2248
                max_cosine_label_inds[ind] = initial_max_cosine_label_inds[ind]
1✔
2249
                pop_orbits.append(ind)
1✔
2250
                pop_labels.append(initial_max_cosine_label_inds[ind])
1✔
2251
            else:
2252
                next_choices = []
1✔
2253
                for grouped_ind in grouped_inds[i]:
1✔
2254
                    j = 1
1✔
2255
                    while True:
2256
                        if max_cosine_orbits_copy[grouped_ind][j][0] not in initial_max_cosine_label_inds:
1✔
2257
                            next_choices.append(max_cosine_orbits_copy[grouped_ind][j][1])
1✔
2258
                            break
1✔
2259
                        j += 1
1✔
2260
                worst_next_choice = next_choices.index(min(next_choices))
1✔
2261
                for grouped_ind in grouped_inds[i]:
1✔
2262
                    if grouped_ind != worst_next_choice:
1✔
2263
                        unassigned_orbits.append(grouped_ind)
1✔
2264
                max_cosine_label_inds[grouped_inds[i][worst_next_choice]] = initial_max_cosine_label_inds[
1✔
2265
                    grouped_inds[i][worst_next_choice]
2266
                ]
2267
                pop_orbits.append(grouped_inds[i][worst_next_choice])
1✔
2268
                pop_labels.append(initial_max_cosine_label_inds[grouped_inds[i][worst_next_choice]])
1✔
2269

2270
        if len(unassigned_orbits) != 0:
1✔
2271
            max_cosine_orbits_copy = self._reduce_cosines_array(max_cosine_orbits_copy, pop_orbits, pop_labels)
1✔
2272
            unassigned_orbits_labels = self._get_orbit_labels(max_cosine_orbits_copy, key_points_inds_orbits, atol)
1✔
2273
            for i, unassigned_orbit in enumerate(unassigned_orbits):
1✔
2274
                max_cosine_label_inds[unassigned_orbit] = unassigned_orbits_labels[i]
1✔
2275

2276
        return max_cosine_label_inds
1✔
2277

2278
    @staticmethod
1✔
2279
    def LabelPoints(index):
1✔
2280
        """
2281
        Axes used in generating labels for Latimer-Munro convention
2282
        """
2283
        points = [
1✔
2284
            [1, 0, 0],
2285
            [0, 1, 0],
2286
            [0, 0, 1],
2287
            [1, 1, 0],
2288
            [1, 0, 1],
2289
            [0, 1, 1],
2290
            [1, 1, 1],
2291
            [1, 2, 0],
2292
            [1, 0, 2],
2293
            [1, 2, 2],
2294
            [2, 1, 0],
2295
            [0, 1, 2],
2296
            [2, 1, 2],
2297
            [2, 0, 1],
2298
            [0, 2, 1],
2299
            [2, 2, 1],
2300
            [1, 1, 2],
2301
            [1, 2, 1],
2302
            [2, 1, 1],
2303
            [3, 3, 2],
2304
            [3, 2, 3],
2305
            [2, 3, 3],
2306
            [2, 2, 2],
2307
            [3, 2, 2],
2308
            [2, 3, 2],
2309
            [1e-10, 1e-10, 1e-10],
2310
        ]
2311

2312
        return points[index]
1✔
2313

2314
    @staticmethod
1✔
2315
    def LabelSymbol(index):
1✔
2316
        """
2317
        Letters used in generating labels for the Latimer-Munro convention
2318
        """
2319
        symbols = [
1✔
2320
            "a",
2321
            "b",
2322
            "c",
2323
            "d",
2324
            "e",
2325
            "f",
2326
            "g",
2327
            "h",
2328
            "i",
2329
            "j",
2330
            "k",
2331
            "l",
2332
            "m",
2333
            "n",
2334
            "o",
2335
            "p",
2336
            "q",
2337
            "r",
2338
            "s",
2339
            "t",
2340
            "u",
2341
            "v",
2342
            "w",
2343
            "x",
2344
            "y",
2345
            "z",
2346
            "Γ",
2347
        ]
2348
        return symbols[index]
1✔
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