# coding: utf-8
# Copyright (c) Pymatgen Development Team.
# Distributed under the terms of the MIT License.
"""
Provides a class for interacting with KPath classes to
generate high-symmetry k-paths using different conventions.
"""
import itertools
import numpy as np
import networkx as nx
from pymatgen.symmetry.kpath import KPathBase, KPathSetyawanCurtarolo, KPathLatimerMunro, KPathSeek
__author__ = "Jason Munro"
__copyright__ = "Copyright 2020, The Materials Project"
__version__ = "0.1"
__maintainer__ = "Jason Munro"
__email__ = "jmunro@lbl.gov"
__status__ = "Development"
__date__ = "March 2020"
[docs]class HighSymmKpath(KPathBase):
"""
This class generates path along high symmetry lines in the
Brillouin zone according to different conventions.
The class is designed to be used with a specific primitive
cell setting. The definitions for the primitive cell
used can be found in: Computational Materials Science,
49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010.
The space group analyzer can be used to produce the correct
primitive structure
(method get_primitive_standard_structure(international_monoclinic=False)).
Ensure input structure is correct before 'get_kpoints()' method is used.
See individual KPath classes for details on specific conventions.
"""
def __init__(
self, structure, has_magmoms=False, magmom_axis=None, path_type="sc",
symprec=0.01, angle_tolerance=5, atol=1e-5):
"""
Args:
structure (Structure): Structure object
has_magmoms (boolean): Whether the input structure contains
magnetic moments as site properties with the key 'magmom.'
Values may be in the form of 3-component vectors given in
the basis of the input lattice vectors, in
which case the spin axis will default to a_3, the third
real-space lattice vector (this triggers a warning).
magmom_axis (list or numpy array): 3-component vector specifying
direction along which magnetic moments given as scalars
should point. If all magnetic moments are provided as
vectors then this argument is not used.
path_type (string): Chooses which convention to use to generate
the high symmetry path. Options are: 'sc', 'hin', 'lm' for the
Setyawan & Curtarolo, Hinuma et al., and Latimer & Munro conventions.
Choosing 'all' will generate one path with points from all three
conventions. Equivalent labels between each will also be generated.
Order will always be Latimer & Munro, Setyawan & Curtarolo, and Hinuma et al.
Lengths for each of the paths will also be generated and output
as a list. Note for 'all' the user will have to alter the labels on
their own for plotting.
symprec (float): Tolerance for symmetry finding
angle_tolerance (float): Angle tolerance for symmetry finding.
atol (float): Absolute tolerance used to determine symmetric
equivalence of points and lines on the BZ.
"""
super().__init__(structure, symprec=symprec, angle_tolerance=angle_tolerance, atol=atol)
self._path_type = path_type
self._equiv_labels = None
self._path_lengths = None
self._label_index = None
if path_type != "all":
if path_type == "lm":
self._kpath = self._get_lm_kpath(has_magmoms, magmom_axis, symprec, angle_tolerance, atol).kpath
elif path_type == "sc":
self._kpath = self._get_sc_kpath(symprec, angle_tolerance, atol).kpath
elif path_type == "hin":
hin_dat = self._get_hin_kpath(symprec, angle_tolerance, atol, not has_magmoms)
self._kpath = hin_dat.kpath
self._hin_tmat = hin_dat._tmat
else:
if has_magmoms:
raise ValueError("Cannot select 'all' with non-zero magmoms.")
lm_bs = self._get_lm_kpath(has_magmoms, magmom_axis, symprec, angle_tolerance, atol)
rpg = lm_bs._rpg
sc_bs = self._get_sc_kpath(symprec, angle_tolerance, atol)
hin_bs = self._get_hin_kpath(symprec, angle_tolerance, atol, not has_magmoms)
index = 0
cat_points = {}
label_index = {}
num_path = []
self._path_lengths = []
for bs in [lm_bs, sc_bs, hin_bs]:
for key, value in enumerate(bs.kpath["kpoints"]):
cat_points[index] = bs.kpath["kpoints"][value]
label_index[index] = value
index += 1
total_points_path = 0
for seg in bs.kpath["path"]:
total_points_path += len(seg)
for block in bs.kpath["path"]:
new_block = []
for label in block:
for ind in range(len(label_index) - len(bs.kpath["kpoints"]), len(label_index),):
if label_index[ind] == label:
new_block.append(ind)
num_path.append(new_block)
self._path_lengths.append(total_points_path)
self._label_index = label_index
self._kpath = {"kpoints": cat_points, "path": num_path}
self._equiv_labels = self._get_klabels(lm_bs, sc_bs, hin_bs, rpg)
@property
def path_type(self):
"""
Returns:
The type of kpath chosen
"""
return self._path_type
@property
def label_index(self):
"""
Returns:
The correspondance between numbers and kpoint symbols for the
combined kpath generated when path_type = 'all'. None otherwise.
"""
return self._label_index
@property
def equiv_labels(self):
"""
Returns:
The correspondance between the kpoint symbols in the Latimer and
Munro convention, Setyawan and Curtarolo, and Hinuma
conventions respectively. Only generated when path_type = 'all'.
"""
return self._equiv_labels
@property
def path_lengths(self):
"""
Returns:
List of lengths of the Latimer and Munro, Setyawan and Curtarolo, and Hinuma
conventions in the combined HighSymmKpath object when path_type = 'all' respectively.
None otherwise.
"""
return self._path_lengths
def _get_lm_kpath(self, has_magmoms, magmom_axis, symprec, angle_tolerance, atol):
"""
Returns:
Latimer and Munro k-path with labels.
"""
return KPathLatimerMunro(self._structure, has_magmoms, magmom_axis, symprec, angle_tolerance, atol)
def _get_sc_kpath(self, symprec, angle_tolerance, atol):
"""
Returns:
Setyawan and Curtarolo k-path with labels.
"""
kpath = KPathSetyawanCurtarolo(self._structure, symprec, angle_tolerance, atol)
self.prim = kpath.prim
self.conventional = kpath.conventional
self.prim_rec = kpath.prim_rec
self._rec_lattice = self.prim_rec
return kpath
def _get_hin_kpath(self, symprec, angle_tolerance, atol, tri):
"""
Returns:
Hinuma et al. k-path with labels.
"""
bs = KPathSeek(self._structure, symprec, angle_tolerance, atol, tri)
kpoints = bs.kpath["kpoints"]
tmat = bs._tmat
for key in kpoints:
kpoints[key] = np.dot(np.transpose(np.linalg.inv(tmat)), kpoints[key])
return bs
def _get_klabels(self, lm_bs, sc_bs, hin_bs, rpg):
"""
Returns:
labels (dict): Dictionary of equivalent labels for paths if 'all' is chosen.
If an exact kpoint match cannot be found, symmetric equivalency will be
searched for and indicated with an asterisk in the equivalent label.
If an equivalent label can still not be found, or the point is not in
the explicit kpath, its equivalent label will be set to itself in the output.
"""
lm_path = lm_bs.kpath
sc_path = sc_bs.kpath
hin_path = hin_bs.kpath
n_op = len(rpg)
pairs = itertools.permutations([{"sc": sc_path}, {"lm": lm_path}, {"hin": hin_path}], r=2)
labels = {"sc": {}, "lm": {}, "hin": {}}
for (a, b) in pairs:
[(a_type, a_path)] = list(a.items())
[(b_type, b_path)] = list(b.items())
sc_count = np.zeros(n_op)
for o_num in range(0, n_op):
a_tr_coord = []
for (label_a, coord_a) in a_path["kpoints"].items():
a_tr_coord.append(np.dot(rpg[o_num], coord_a))
for coord_a in a_tr_coord:
for key, value in b_path["kpoints"].items():
if np.allclose(value, coord_a, atol=self._atol):
sc_count[o_num] += 1
break
a_to_b_labels = {}
unlabeled = {}
for (label_a, coord_a) in a_path["kpoints"].items():
coord_a_t = np.dot(rpg[np.argmax(sc_count)], coord_a)
assigned = False
for (label_b, coord_b) in b_path["kpoints"].items():
if np.allclose(coord_b, coord_a_t, atol=self._atol):
a_to_b_labels[label_a] = label_b
assigned = True
break
if not assigned:
unlabeled[label_a] = coord_a
for (label_a, coord_a) in unlabeled.items():
for op in rpg:
coord_a_t = np.dot(op, coord_a)
key = [
key
for key, value in b_path["kpoints"].items()
if np.allclose(value, coord_a_t, atol=self._atol)
]
if key != []:
a_to_b_labels[label_a] = key[0][0] + "^{*}"
break
if key == []:
a_to_b_labels[label_a] = label_a
labels[a_type][b_type] = a_to_b_labels
return labels
[docs] @staticmethod
def get_continuous_path(bandstructure):
"""
Obtain a continous version of an inputted path using graph theory.
This routine will attempt to add connections between nodes of
odd-degree to ensure a Eulerian path can be formed. Initial
k-path must be able to be converted to a connected graph.
Args:
bandstructure (Bandstructure): Bandstructure object.
Returns:
distances_map (list): Mapping of 'distance' segments for altering a
BSPlotter object to new continuous path. List of tuples indicating the
new order of distances, and whether they should be plotted in reverse.
kpath_euler (list): New continuous kpath in the HighSymmKpath format.
"""
G = nx.Graph()
labels = []
for point in bandstructure.kpoints:
if point.label is not None:
labels.append(point.label)
plot_axis = []
for i in range(int(len(labels) / 2)):
G.add_edges_from([(labels[2 * i], labels[(2 * i) + 1])])
plot_axis.append((labels[2 * i], labels[(2 * i) + 1]))
G_euler = nx.algorithms.euler.eulerize(G)
G_euler_circuit = nx.algorithms.euler.eulerian_circuit(G_euler)
distances_map = []
kpath_euler = []
for edge_euler in G_euler_circuit:
kpath_euler.append(edge_euler)
for edge_reg in plot_axis:
if edge_euler == edge_reg:
distances_map.append((plot_axis.index(edge_reg), False))
elif edge_euler[::-1] == edge_reg:
distances_map.append((plot_axis.index(edge_reg), True))
return distances_map, kpath_euler