#!/usr/bin/env python
r"""
A collection of functions for creating k-points input for different
first-principles codes.
All functions have the same calling structure as :func:`prototype`.
"""
import decorator
import numpy as np
__all__ = [
"prototype",
"abinit",
"qe",
"qe_explicit",
"wannier90",
"wannier90_nnkpts",
"wannier90_full",
"vasp",
]
[docs]
def prototype(kpt):
r"""
Specifies the interface
:param kpt: The list of k-points in the string INCLUDING the
final point which should not be in the calculation
:type kpt: :py:class:`list` of :py:obj:`numpy.array`
"""
raise NotImplementedError("This is only the prototype for kpts")
@decorator.decorator
def _check_dim(fct, kpt):
"""Checks if all k-points are three-dimensional."""
for k in kpt:
if len(k) != 3:
raise ValueError(f"Dimension of point k = {k} != 3")
return fct(kpt)
@decorator.decorator
def _check_closed(fct, kpt):
"""Checks whether the k-point list forms a closed loop."""
delta = kpt[-1] - kpt[0]
if not np.isclose(np.round(delta), delta).all():
raise ValueError("The k-point line does not form a closed loop.")
return fct(kpt)
[docs]
@_check_dim
@_check_closed
def abinit(kpt):
"""
Creates a k-point input for **ABINIT**. It uses ``kptopt -1`` and specifies the k-points string using ``ndivk`` and ``kptbounds``.
"""
_check_equal_spacing(kpt, "ABINIT")
start_point = kpt[0]
last_point = kpt[-2]
num_kpt = len(kpt) - 1
string = "\nkptopt -1\nndivk " + str(int(num_kpt - 1)) + "\nkptbounds "
for coord in start_point:
string += str(coord).replace("e", "d") + " "
string += "\n"
for coord in last_point:
string += str(coord).replace("e", "d") + " "
string += "\n"
return string
[docs]
@_check_dim
@_check_closed
def qe(kpt): # pylint: disable=invalid-name
"""
Creates a k-point input for **Quantum Espresso**.
"""
start_point = kpt[0]
last_point = kpt[-2]
num_kpt = len(kpt) - 1
string = "\nK_POINTS crystal_b\n 2 \n"
for coord in start_point:
string += str(coord).replace("e", "d") + " "
string += str(num_kpt - 1) + "\n"
for coord in last_point:
string += str(coord).replace("e", "d") + " "
string += str(1) + "\n"
return string
[docs]
@_check_dim
@_check_closed
def qe_explicit(kpt):
"""
Creates a k-point input for **Quantum Espresso**, by explicitly specifying the k-points.
"""
num_kpt = len(kpt) - 1
string = f"\nK_POINTS crystal\n {num_kpt} \n"
kpt_str = ((str(coord).replace("e", "d") for coord in k) for k in kpt)
for k in kpt_str:
string += "{} {} {} 1\n".format(*k)
return string
[docs]
@_check_dim
@_check_closed
def wannier90(kpt):
"""
Creates a k-point input for **Wannier90**. It can be useful when the first-principles code does not generate the k-points in ``wannier90.win`` (e.g. with Quantum Espresso).
"""
num_kpt = len(kpt) - 1
string = "mp_grid: " + str(int(num_kpt)) + " 1 1 \nbegin kpoints"
for k in kpt[:-1]:
string += "\n"
for coord in k:
string += str(coord).replace("e", "d") + " "
string += "\nend kpoints\n"
return string
[docs]
@_check_dim
@_check_closed
def wannier90_nnkpts(kpt):
"""
Creates the nnkpts input to explicitly specify the nearest neighbours in wannier90.win
"""
num_kpt = len(kpt) - 1
bz_diff = [np.zeros(3, dtype=int) for _ in range(num_kpt - 1)]
# check whether the last k-point is in a different UC
bz_diff.append(np.array(np.round(kpt[-1] - kpt[0]), dtype=int))
string = "begin nnkpts\n"
for i, k in enumerate(bz_diff):
j = (i + 1) % num_kpt
string += " {0:>3} {1:>3} {2[0]: } {2[1]: } {2[2]: }\n".format(i + 1, j + 1, k)
string += "end nnkpts\n"
return string
[docs]
@_check_dim
@_check_closed
def wannier90_full(kpt):
"""
Returns both k-point and nearest neighbour input for wannier90.win. This is the recommended function to use for Wannier90 2.1 and higher.
"""
return wannier90(kpt) + "\n" + wannier90_nnkpts(kpt)
[docs]
@_check_dim
@_check_closed
def vasp(kpt):
"""
Creates a k-point input for **VASP**. It uses the automatic generation scheme with a Gamma centered grid. Note that VASP does **not** support any kind of k-point line **unless** they are exactly along one of the reciprocal lattice vectors, and the k-points are evenly spaced.
"""
# VALIDITY CHECKS
# check if the points are equally-spaced
delta = _check_equal_spacing(kpt, "VASP")
num_kpt = len(kpt) - 1
# check if it's positive x, y or z direction
nonzero = []
mesh = []
for i, spacing in enumerate(delta):
if np.isclose(spacing, 0):
mesh.append("1")
elif np.isclose(spacing, 1 / num_kpt):
nonzero.append(i)
mesh.append(str(num_kpt))
else:
raise ValueError(
"The k-points must be aligned in (positive) kx-, ky- or kz-direction for VASP runs."
)
mesh_str = " ".join(mesh)
if len(nonzero) != 1:
raise ValueError(
"The k-points can change only in kx-, ky- or kz direction for VASP runs. The given k-points change in {} directions.".format(
len(nonzero)
)
)
start_point = kpt[0]
if not np.isclose(start_point[nonzero[0]], 0):
raise ValueError(
"The k-points must start at k{0} = 0 for VASP runs, since they change in k{0}-direction.".format(
["x", "y", "z"][nonzero[0]]
)
)
string = "Automatic mesh\n0 ! number of k-points = 0 ->automatic generation scheme\nGamma ! generate a Gamma centered grid\n"
string += mesh_str + " ! subdivisions\n"
for coord in start_point:
string += str(coord).replace("e", "d") + " "
string += " ! shift\n"
return string
def _check_equal_spacing(kpt, run_type):
"""Checks if the k-points are equally spaced, and throws an error if not. run_type is added in the error message."""
deltas = [(k2 - k1) % 1 for k2, k1 in zip(kpt[1:], kpt[:-1])]
for spacing in deltas[1:]:
if not np.isclose(spacing, deltas[0]).all():
raise ValueError(f"The k-points must be equally spaced for {run_type} runs.")
return deltas[0]