• 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

91.96
/pymatgen/symmetry/bandstructure.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3
"""
1✔
4
Provides a class for interacting with KPath classes to
5
generate high-symmetry k-paths using different conventions.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import itertools
1✔
11
from warnings import warn
1✔
12

13
import networkx as nx
1✔
14
import numpy as np
1✔
15

16
from pymatgen.electronic_structure.bandstructure import BandStructureSymmLine
1✔
17
from pymatgen.electronic_structure.core import Spin
1✔
18
from pymatgen.symmetry.kpath import (
1✔
19
    KPathBase,
20
    KPathLatimerMunro,
21
    KPathSeek,
22
    KPathSetyawanCurtarolo,
23
)
24

25
__author__ = "Jason Munro"
1✔
26
__copyright__ = "Copyright 2020, The Materials Project"
1✔
27
__version__ = "0.1"
1✔
28
__maintainer__ = "Jason Munro"
1✔
29
__email__ = "jmunro@lbl.gov"
1✔
30
__status__ = "Development"
1✔
31
__date__ = "March 2020"
1✔
32

33

34
class HighSymmKpath(KPathBase):
1✔
35
    """
36
    This class generates path along high symmetry lines in the
37
    Brillouin zone according to different conventions.
38
    The class is designed to be used with a specific primitive
39
    cell setting. The definitions for the primitive cell
40
    used can be found in: Computational Materials Science,
41
    49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010.
42
    The space group analyzer can be used to produce the correct
43
    primitive structure
44
    (method get_primitive_standard_structure(international_monoclinic=False)).
45
    Ensure input structure is correct before 'get_kpoints()' method is used.
46
    See individual KPath classes for details on specific conventions.
47
    """
48

49
    def __init__(
1✔
50
        self,
51
        structure,
52
        has_magmoms=False,
53
        magmom_axis=None,
54
        path_type="setyawan_curtarolo",
55
        symprec=0.01,
56
        angle_tolerance=5,
57
        atol=1e-5,
58
    ):
59
        """
60
        Args:
61
            structure (Structure): Structure object
62
            has_magmoms (bool): Whether the input structure contains
63
                magnetic moments as site properties with the key 'magmom.'
64
                Values may be in the form of 3-component vectors given in
65
                the basis of the input lattice vectors, in
66
                which case the spin axis will default to a_3, the third
67
                real-space lattice vector (this triggers a warning).
68
            magmom_axis (list or numpy array): 3-component vector specifying
69
                direction along which magnetic moments given as scalars
70
                should point. If all magnetic moments are provided as
71
                vectors then this argument is not used.
72
            path_type (str): Chooses which convention to use to generate
73
                the high symmetry path. Options are: 'setyawan_curtarolo', 'hinuma',
74
                'latimer_munro' for the Setyawan & Curtarolo, Hinuma et al., and
75
                Latimer & Munro conventions. Choosing 'all' will generate one path
76
                with points from all three conventions. Equivalent labels between
77
                each will also be generated. Order will always be Latimer & Munro,
78
                Setyawan & Curtarolo, and Hinuma et al. Lengths for each of the paths
79
                will also be generated and output as a list. Note for 'all' the user
80
                will have to alter the labels on their own for plotting.
81
            symprec (float): Tolerance for symmetry finding
82
            angle_tolerance (float): Angle tolerance for symmetry finding.
83
            atol (float): Absolute tolerance used to determine symmetric
84
                equivalence of points and lines on the BZ.
85
        """
86
        super().__init__(structure, symprec=symprec, angle_tolerance=angle_tolerance, atol=atol)
1✔
87

88
        self._path_type = path_type
1✔
89

90
        self._equiv_labels = None
1✔
91
        self._path_lengths = None
1✔
92
        self._label_index = None
1✔
93

94
        if path_type != "all":
1✔
95
            if path_type == "latimer_munro":
1✔
96
                self._kpath = self._get_lm_kpath(has_magmoms, magmom_axis, symprec, angle_tolerance, atol).kpath
×
97
            elif path_type == "setyawan_curtarolo":
1✔
98
                self._kpath = self._get_sc_kpath(symprec, angle_tolerance, atol).kpath
1✔
99
            elif path_type == "hinuma":
×
100
                hin_dat = self._get_hin_kpath(symprec, angle_tolerance, atol, not has_magmoms)
×
101
                self._kpath = hin_dat.kpath
×
102
                self._hin_tmat = hin_dat._tmat
×
103

104
        else:
105
            if has_magmoms:
1✔
106
                raise ValueError("Cannot select 'all' with non-zero magmoms.")
×
107

108
            lm_bs = self._get_lm_kpath(has_magmoms, magmom_axis, symprec, angle_tolerance, atol)
1✔
109
            rpg = lm_bs._rpg
1✔
110

111
            sc_bs = self._get_sc_kpath(symprec, angle_tolerance, atol)
1✔
112
            hin_bs = self._get_hin_kpath(symprec, angle_tolerance, atol, not has_magmoms)
1✔
113

114
            index = 0
1✔
115
            cat_points = {}
1✔
116
            label_index = {}
1✔
117
            num_path = []
1✔
118
            self._path_lengths = []
1✔
119

120
            for bs in [lm_bs, sc_bs, hin_bs]:
1✔
121
                for value in bs.kpath["kpoints"]:
1✔
122
                    cat_points[index] = bs.kpath["kpoints"][value]
1✔
123
                    label_index[index] = value
1✔
124
                    index += 1
1✔
125

126
                total_points_path = 0
1✔
127
                for seg in bs.kpath["path"]:
1✔
128
                    total_points_path += len(seg)
1✔
129

130
                for block in bs.kpath["path"]:
1✔
131
                    new_block = []
1✔
132
                    for label in block:
1✔
133
                        for ind in range(
1✔
134
                            len(label_index) - len(bs.kpath["kpoints"]),
135
                            len(label_index),
136
                        ):
137
                            if label_index[ind] == label:
1✔
138
                                new_block.append(ind)
1✔
139

140
                    num_path.append(new_block)
1✔
141

142
                self._path_lengths.append(total_points_path)
1✔
143

144
            self._label_index = label_index
1✔
145

146
            self._kpath = {"kpoints": cat_points, "path": num_path}
1✔
147

148
            self._equiv_labels = self._get_klabels(lm_bs, sc_bs, hin_bs, rpg)
1✔
149

150
    @property
1✔
151
    def path_type(self):
1✔
152
        """
153
        Returns:
154
        The type of kpath chosen
155
        """
156
        return self._path_type
×
157

158
    @property
1✔
159
    def label_index(self):
1✔
160
        """
161
        Returns:
162
        The correspondence between numbers and kpoint symbols for the
163
        combined kpath generated when path_type = 'all'. None otherwise.
164
        """
165
        return self._label_index
×
166

167
    @property
1✔
168
    def equiv_labels(self):
1✔
169
        """
170
        Returns:
171
        The correspondence between the kpoint symbols in the Latimer and
172
        Munro convention, Setyawan and Curtarolo, and Hinuma
173
        conventions respectively. Only generated when path_type = 'all'.
174
        """
175
        return self._equiv_labels
×
176

177
    @property
1✔
178
    def path_lengths(self):
1✔
179
        """
180
        Returns:
181
        List of lengths of the Latimer and Munro, Setyawan and Curtarolo, and Hinuma
182
        conventions in the combined HighSymmKpath object when path_type = 'all' respectively.
183
        None otherwise.
184
        """
185
        return self._path_lengths
×
186

187
    def _get_lm_kpath(self, has_magmoms, magmom_axis, symprec, angle_tolerance, atol):
1✔
188
        """
189
        Returns:
190
        Latimer and Munro k-path with labels.
191
        """
192
        return KPathLatimerMunro(self._structure, has_magmoms, magmom_axis, symprec, angle_tolerance, atol)
1✔
193

194
    def _get_sc_kpath(self, symprec, angle_tolerance, atol):
1✔
195
        """
196
        Returns:
197
        Setyawan and Curtarolo k-path with labels.
198
        """
199
        kpath = KPathSetyawanCurtarolo(self._structure, symprec, angle_tolerance, atol)
1✔
200

201
        self.prim = kpath.prim
1✔
202
        self.conventional = kpath.conventional
1✔
203
        self.prim_rec = kpath.prim_rec
1✔
204
        self._rec_lattice = self.prim_rec
1✔
205

206
        return kpath
1✔
207

208
    def _get_hin_kpath(self, symprec, angle_tolerance, atol, tri):
1✔
209
        """
210
        Returns:
211
        Hinuma et al. k-path with labels.
212
        """
213
        bs = KPathSeek(self._structure, symprec, angle_tolerance, atol, tri)
1✔
214

215
        kpoints = bs.kpath["kpoints"]
1✔
216
        tmat = bs._tmat
1✔
217
        for key in kpoints:
1✔
218
            kpoints[key] = np.dot(np.transpose(np.linalg.inv(tmat)), kpoints[key])
1✔
219

220
        bs.kpath["kpoints"] = kpoints
1✔
221
        self._rec_lattice = self._structure.lattice.reciprocal_lattice
1✔
222

223
        warn(
1✔
224
            "K-path from the Hinuma et al. convention has been transformed to the basis of the reciprocal lattice"
225
            "of the input structure. Use `KPathSeek` for the path in the original author-intended basis."
226
        )
227

228
        return bs
1✔
229

230
    def _get_klabels(self, lm_bs, sc_bs, hin_bs, rpg):
1✔
231
        """
232
        Returns:
233
        labels (dict): Dictionary of equivalent labels for paths if 'all' is chosen.
234
            If an exact kpoint match cannot be found, symmetric equivalency will be
235
            searched for and indicated with an asterisk in the equivalent label.
236
            If an equivalent label can still not be found, or the point is not in
237
            the explicit kpath, its equivalent label will be set to itself in the output.
238
        """
239
        lm_path = lm_bs.kpath
1✔
240
        sc_path = sc_bs.kpath
1✔
241
        hin_path = hin_bs.kpath
1✔
242

243
        n_op = len(rpg)
1✔
244

245
        pairs = itertools.permutations(
1✔
246
            [{"setyawan_curtarolo": sc_path}, {"latimer_munro": lm_path}, {"hinuma": hin_path}], r=2
247
        )
248
        labels = {"setyawan_curtarolo": {}, "latimer_munro": {}, "hinuma": {}}
1✔
249

250
        for a, b in pairs:
1✔
251
            [(a_type, a_path)] = list(a.items())
1✔
252
            [(b_type, b_path)] = list(b.items())
1✔
253

254
            sc_count = np.zeros(n_op)
1✔
255

256
            for o_num in range(0, n_op):
1✔
257
                a_tr_coord = []
1✔
258

259
                for coord_a in a_path["kpoints"].values():
1✔
260
                    a_tr_coord.append(np.dot(rpg[o_num], coord_a))
1✔
261

262
                for coord_a in a_tr_coord:
1✔
263
                    for value in b_path["kpoints"].values():
1✔
264
                        if np.allclose(value, coord_a, atol=self._atol):
1✔
265
                            sc_count[o_num] += 1
1✔
266
                            break
1✔
267

268
            a_to_b_labels = {}
1✔
269
            unlabeled = {}
1✔
270

271
            for label_a, coord_a in a_path["kpoints"].items():
1✔
272
                coord_a_t = np.dot(rpg[np.argmax(sc_count)], coord_a)
1✔
273
                assigned = False
1✔
274

275
                for label_b, coord_b in b_path["kpoints"].items():
1✔
276
                    if np.allclose(coord_b, coord_a_t, atol=self._atol):
1✔
277
                        a_to_b_labels[label_a] = label_b
1✔
278
                        assigned = True
1✔
279
                        break
1✔
280

281
                if not assigned:
1✔
282
                    unlabeled[label_a] = coord_a
1✔
283

284
            for label_a, coord_a in unlabeled.items():
1✔
285
                for op in rpg:
1✔
286
                    coord_a_t = np.dot(op, coord_a)
1✔
287
                    key = [
1✔
288
                        key
289
                        for key, value in b_path["kpoints"].items()
290
                        if np.allclose(value, coord_a_t, atol=self._atol)
291
                    ]
292

293
                    if key != []:
1✔
294
                        a_to_b_labels[label_a] = key[0][0] + "^{*}"
1✔
295
                        break
1✔
296

297
                if key == []:
1✔
298
                    a_to_b_labels[label_a] = label_a
1✔
299

300
            labels[a_type][b_type] = a_to_b_labels
1✔
301

302
        return labels
1✔
303

304
    @staticmethod
1✔
305
    def get_continuous_path(bandstructure):
1✔
306
        """
307
        Obtain a continuous version of an inputted path using graph theory.
308
        This routine will attempt to add connections between nodes of
309
        odd-degree to ensure a Eulerian path can be formed. Initial
310
        k-path must be able to be converted to a connected graph. See
311
        npj Comput Mater 6, 112 (2020). 10.1038/s41524-020-00383-7
312
        for more details.
313

314
        Args:
315
        bandstructure (BandstructureSymmLine): BandstructureSymmLine object.
316

317
        Returns:
318
        bandstructure (BandstructureSymmLine): New BandstructureSymmLine object with continuous path.
319
        """
320
        G = nx.Graph()
1✔
321

322
        labels = []
1✔
323
        for point in bandstructure.kpoints:
1✔
324
            if point.label is not None:
1✔
325
                labels.append(point.label)
1✔
326

327
        plot_axis = []
1✔
328
        for i in range(int(len(labels) / 2)):
1✔
329
            G.add_edges_from([(labels[2 * i], labels[(2 * i) + 1])])
1✔
330
            plot_axis.append((labels[2 * i], labels[(2 * i) + 1]))
1✔
331

332
        G_euler = nx.algorithms.euler.eulerize(G)
1✔
333

334
        G_euler_circuit = nx.algorithms.euler.eulerian_circuit(G_euler)
1✔
335

336
        distances_map = []
1✔
337
        kpath_euler = []
1✔
338

339
        for edge_euler in G_euler_circuit:
1✔
340
            kpath_euler.append(edge_euler)
1✔
341
            for edge_reg in plot_axis:
1✔
342
                if edge_euler == edge_reg:
1✔
343
                    distances_map.append((plot_axis.index(edge_reg), False))
1✔
344
                elif edge_euler[::-1] == edge_reg:
1✔
345
                    distances_map.append((plot_axis.index(edge_reg), True))
1✔
346

347
        if bandstructure.is_spin_polarized:
1✔
348
            spins = [Spin.up, Spin.down]
×
349
        else:
350
            spins = [Spin.up]
1✔
351

352
        new_kpoints = []
1✔
353
        new_bands = {spin: [np.array([]) for _ in range(bandstructure.nb_bands)] for spin in spins}
1✔
354
        new_projections = {spin: [[] for _ in range(bandstructure.nb_bands)] for spin in spins}
1✔
355

356
        num_branches = len(bandstructure.branches)
1✔
357
        new_branches = []
1✔
358

359
        # This ensures proper format of bandstructure.branches
360
        processed = []
1✔
361
        for ind in range(num_branches):
1✔
362
            branch = bandstructure.branches[ind]
1✔
363

364
            if branch["name"] not in processed:
1✔
365
                if tuple(branch["name"].split("-")) in plot_axis:
1✔
366
                    new_branches.append(branch)
1✔
367
                    processed.append(branch["name"])
1✔
368
                else:
369
                    next_branch = bandstructure.branches[ind + 1]
×
370
                    combined = {
×
371
                        "start_index": branch["start_index"],
372
                        "end_index": next_branch["end_index"],
373
                        "name": f"{branch['name'].split('-')[0]}-{next_branch['name'].split('-')[1]}",
374
                    }
375
                    processed.append(branch["name"])
×
376
                    processed.append(next_branch["name"])
×
377

378
                    new_branches.append(combined)
×
379

380
        # Obtain new values
381
        for entry in distances_map:
1✔
382
            branch = new_branches[entry[0]]
1✔
383

384
            if not entry[1]:
1✔
385
                start = branch["start_index"]
1✔
386
                stop = branch["end_index"] + 1
1✔
387
                step = 1
1✔
388
            else:
389
                start = branch["end_index"]
1✔
390
                stop = branch["start_index"] - 1 if branch["start_index"] != 0 else None
1✔
391
                step = -1
1✔
392

393
            # kpoints
394
            new_kpoints += [point.frac_coords for point in bandstructure.kpoints[start:stop:step]]
1✔
395

396
            # eigenvals
397
            for spin in spins:
1✔
398
                for n, band in enumerate(bandstructure.bands[spin]):
1✔
399
                    new_bands[spin][n] = np.concatenate((new_bands[spin][n], band[start:stop:step]))
1✔
400

401
            # projections
402
            for spin in spins:
1✔
403
                for n, band in enumerate(bandstructure.projections[spin]):
1✔
404
                    new_projections[spin][n] += band[start:stop:step].tolist()
1✔
405

406
        for spin in spins:
1✔
407
            new_projections[spin] = np.array(new_projections[spin])
1✔
408

409
        new_labels_dict = {label: point.frac_coords for label, point in bandstructure.labels_dict.items()}
1✔
410

411
        new_bandstructure = BandStructureSymmLine(
1✔
412
            kpoints=new_kpoints,
413
            eigenvals=new_bands,
414
            lattice=bandstructure.lattice_rec,
415
            efermi=bandstructure.efermi,
416
            labels_dict=new_labels_dict,
417
            structure=bandstructure.structure,
418
            projections=new_projections,
419
        )
420

421
        return new_bandstructure
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