"""
This module generates kpoint-weights for uniform-mesh non-magnetic materials.
This is vital when using the Materials Project database to generate spectra in
PyTASER.
"""
import numpy as np
from pymatgen.core import Structure
from pymatgen.electronic_structure.bandstructure import BandStructureSymmLine
[docs]
def get_kpoint_weights(bandstructure, time_reversal=True, symprec=0.1):
"""
Function to calculate the kpoint_weights for non-magnetic materials (non-
metals).
Args:
bandstructure: PMG bandstructure object
time_reversal: Time reversal operator, bool (True for +1, False for -1)
symprec: Symmetry precision in Angstrom.(Lower value is more precise, but
computationally more expensive)
Returns:
k-point_weights
"""
if isinstance(bandstructure, BandStructureSymmLine):
raise TypeError(
"This bandstructure object uses a line-mode kpoint mesh instead of a uniform mesh. Kpoint "
"weighting is not possible"
)
kpoints = get_kpoints_from_bandstructure(bandstructure)
_, _, _, _, _, kp_mapping = expand_kpoints(
bandstructure.structure,
kpoints,
symprec=symprec,
time_reversal=time_reversal,
return_mapping=True,
)
weights = np.unique(kp_mapping, return_counts=True)[1].astype(float)
weights /= np.sum(weights)
return weights
[docs]
def get_kpoints_from_bandstructure(bandstructure, cartesian=False):
"""
Function to pull the kpoint from the bandstructure.
Args:
bandstructure: PMG bandstructure object
cartesian: bool, indicate if cartesian or fractional coordinates used.
Returns:
k-points
"""
if cartesian:
kpoints = np.array([k.cart_coords for k in bandstructure.kpoints])
else:
kpoints = np.array([k.frac_coords for k in bandstructure.kpoints])
return kpoints
[docs]
def expand_kpoints(
structure,
kpoints,
symprec=0.01,
return_mapping=False,
time_reversal=True,
):
"""
Function to expand the kpoints.
Args:
structure: PMG structure object
kpoints: uniform mesh kpoints array.
symprec: Symmetry precision in Angstrom.(Lower value is more precise, but
computationally more expensive)
return_mapping: bool
time_reversal: Time reversal operator, bool (True for +1, False for -1)
Returns:
full_kpoints, rotations, translations, is_tr, op_mapping, kp_mapping
"""
kpoints = np.array(kpoints).round(8)
# due to limited input precision of the k-points, the mesh is returned as a float
mesh, is_shifted = get_mesh_from_kpoint_diff(kpoints)
shift = np.array([1, 1, 1]) if is_shifted else np.array([0, 0, 0])
# to avoid issues to limited input precision, recalculate the input k-points
# so that the mesh is integer and the k-points are not truncated
# to a small precision
addresses = np.rint((kpoints + shift / (mesh * 2)) * mesh)
mesh = np.rint(mesh)
kpoints = addresses / mesh - shift / (mesh * 2)
rotations, translations, is_tr = get_reciprocal_point_group_operations(
structure, symprec=symprec, time_reversal=time_reversal
)
len(rotations)
# rotate all-kpoints
all_rotated_kpoints = []
for r in rotations:
all_rotated_kpoints.append(np.dot(r, kpoints.T).T)
all_rotated_kpoints = np.concatenate(all_rotated_kpoints)
# map to first BZ
all_rotated_kpoints -= np.rint(all_rotated_kpoints)
all_rotated_kpoints = all_rotated_kpoints.round(8)
# zone boundary consistent with VASP not with spglib
all_rotated_kpoints[all_rotated_kpoints == -0.5] = 0.5
# Find unique points
unique_rotated_kpoints, unique_idxs = np.unique(all_rotated_kpoints, return_index=True, axis=0)
# find integer addresses
unique_addresses = (unique_rotated_kpoints + shift / (mesh * 2)) * mesh
unique_addresses -= np.rint(unique_addresses)
in_uniform_mesh = (np.abs(unique_addresses) < 1e-5).all(axis=1)
n_mapped = int(np.sum(in_uniform_mesh))
n_expected = int(np.prod(mesh))
if n_mapped != n_expected:
raise ValueError(f"Expected {n_expected} points but found {n_mapped}")
full_kpoints = unique_rotated_kpoints[in_uniform_mesh]
full_idxs = unique_idxs[in_uniform_mesh]
if not return_mapping:
return full_kpoints
op_mapping = np.floor(full_idxs / len(kpoints)).astype(int)
kp_mapping = (full_idxs % len(kpoints)).astype(int)
return full_kpoints, rotations, translations, is_tr, op_mapping, kp_mapping
[docs]
def get_mesh_from_kpoint_diff(kpoints, ktol=1e-5):
"""
Function to get the uniform mesh from kpoint differences.
Args:
kpoints: uniform mesh kpoints array.
ktol: threshold for filtering changes in kpoint-mesh points.
Returns:
np.array([na, nb, nc]), is_shifted
"""
kpoints = np.array(kpoints)
# whether the k-point mesh is shifted or Gamma centered mesh
is_shifted = np.min(np.linalg.norm(kpoints, axis=1)) > 1e-6
unique_a = np.unique(kpoints[:, 0])
unique_b = np.unique(kpoints[:, 1])
unique_c = np.unique(kpoints[:, 2])
if len(unique_a) == 1:
na = 1
else:
# filter very small changes, with a tol of 5e-4 this means k-point meshes
# denser than 2000x2000x2000 will be treated as numerical noise. Meshes
# this dense are extremely unlikely
diff = np.diff(unique_a)
diff = diff[diff > ktol]
na = 1 / np.min(diff[diff > ktol])
if len(unique_b) == 1:
nb = 1
else:
diff = np.diff(unique_b)
nb = 1 / np.min(diff[diff > ktol])
if len(unique_c) == 1:
nc = 1
else:
diff = np.diff(unique_c)
nc = 1 / np.min(diff[diff > ktol])
# due to limited precision of the input k-points, the mesh is returned as a float
return np.array([na, nb, nc]), is_shifted
[docs]
def get_reciprocal_point_group_operations(
structure: Structure,
symprec: float = 0.01,
time_reversal: bool = True,
):
"""
Function to get the reciprocal point group operations.
Args:
structure: PMG structure object
symprec: Symmetry precision in Angstrom.(Lower value is more precise, but
computationally more expensive)
time_reversal: Time reversal operator, bool (True for +1, False for -1)
Returns:
rotations[sort_idx], translations[sort_idx], is_tr[sort_idx]
"""
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
sga = SpacegroupAnalyzer(structure, symprec=symprec)
if sga.get_symmetry_dataset() is None:
# sometimes default angle tolerance doesn't work as expected
sga = SpacegroupAnalyzer(structure, symprec=symprec, angle_tolerance=-1)
rotations = sga.get_symmetry_dataset()["rotations"].transpose((0, 2, 1))
translations = sga.get_symmetry_dataset()["translations"]
is_tr = np.full(len(rotations), False, dtype=bool)
if time_reversal:
rotations = np.concatenate([rotations, -rotations])
translations = np.concatenate([translations, -translations])
is_tr = np.concatenate([is_tr, ~is_tr])
rotations, unique_ops = np.unique(rotations, axis=0, return_index=True)
translations = translations[unique_ops]
is_tr = is_tr[unique_ops]
# put identity first and time-reversal last
sort_idx = np.argsort(np.abs(rotations - np.eye(3)).sum(axis=(1, 2)) + is_tr * 10)
return rotations[sort_idx], translations[sort_idx], is_tr[sort_idx]