Source code for abipy.core.kpoints

# coding: utf-8
"""This module defines objects describing the sampling of the Brillouin Zone."""
from __future__ import annotations

import collections
import json
import sys
import time
import numpy as np

from itertools import product
from tabulate import tabulate
from monty.collections import AttrDict, dict2namedtuple
from monty.functools import lazy_property
from monty.string import marquee
from abipy.tools.serialization import pmg_serialize
from abipy.iotools import ETSF_Reader
from abipy.tools.derivatives import finite_diff
from abipy.tools.numtools import add_periodic_replicas, is_diagonal
from abipy.core.mixins import SlotPickleMixin

import logging
logger = logging.getLogger(__name__)

__all__ = [
    "issamek",
    "wrap_to_ws",
    "wrap_to_bz",
    "as_kpoints",
    "Kpoint",
    "KpointList",
    "KpointStar",
    "Kpath",
    "IrredZone",
    "rc_list",
    "kmesh_from_mpdivs",
    "Ktables",
    "find_points_along_path",
]

# Tolerance used to compare k-points.
_ATOL_KDIFF = 1e-8

# Tolerances passed to spglib.
_SPGLIB_SYMPREC = 1e-5
_SPGLIB_ANGLE_TOLERANCE = -1.0


def set_atol_kdiff(new_atol):
    """
    Change the value of the tolerance ``_ATOL_KDIFF`` used to compare k-points.
    Return old value

    .. warning::

        This function should be called at the beginning of the script.
    """
    global _ATOL_KDIFF
    old_atol = _ATOL_KDIFF
    _ATOL_KDIFF = new_atol
    return old_atol


def set_spglib_tols(symprec, angle_tolerance):
    """
    Change the value of the tolerances ``symprec`` and ``angle_tolerance``
    used to call spglib_. Return old values

    .. warning::

        This function should be called at the beginning of the script.
    """
    global _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE
    old_symprec, old_angle_tolerance = _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE
    _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE = symprec, angle_tolerance
    return old_symprec, old_angle_tolerance


def is_integer(x, atol=None):
    """
    True if all x is integer within the absolute tolerance atol.
    Use _ATOL_KDIFF is atol is None.

    >>> assert is_integer([1., 2.])
    >>> assert is_integer(1.01, atol=0.011)
    >>> assert not is_integer([1.01, 2])
    """
    if atol is None: atol = _ATOL_KDIFF
    int_x = np.around(x)
    return np.allclose(int_x, x, atol=atol)


[docs] def issamek(k1, k2, atol=None): """ True if k1 and k2 are equal modulo a lattice vector. Use _ATOL_KDIFF is atol is None. >>> assert issamek([1, 1, 1], [0, 0, 0]) >>> assert issamek([1.1, 1, 1], [0, 0, 0], atol=0.1) >>> assert not issamek(0.00003, 1) """ k1 = np.asarray(k1) k2 = np.asarray(k2) return is_integer(k1 - k2, atol=atol)
[docs] def wrap_to_ws(x): """ Transforms x in its corresponding reduced number in the interval ]-1/2,1/2]. """ w = x % 1 return np.where(w > 0.5, w - 1.0, w)
[docs] def wrap_to_bz(x): """ Transforms x in its corresponding reduced number in the interval [0,1[." """ return x % 1
[docs] def rc_list(mp, sh, pbc=False, order="bz"): """ Returns a |numpy-array| with the linear mesh used to sample one dimension of the reciprocal space. Note that the coordinates are always ordered so that rc[i+1] > rc[i]. so that we can easily plot quantities defined on the 3D multidimensional mesh. Args: mp: Number of Monkhorst-Pack divisions along this direction. sh: Shift pbc: if pbc is True, periodic boundary conditions are enforced. order: Possible values ["bz", "unit_cell"]. if "bz", the coordinates are forced to be in [-1/2, 1/2) if "unit_cell", the coordinates are forced to be in [0, 1). """ rc = [] if order == "unit_cell": n = mp if not pbc else mp + 1 for i in range(n): rc.append((i + sh) / mp) elif order == "bz": for i in range(mp): x = (i + sh) / mp if x < 0.5: rc.append(x) else: # Insert xm1 in rc so that we still have a ordered list. xm1 = x - 1.0 for i, c in enumerate(rc): if c > xm1: break else: raise ValueError("Error while inserting xm1") rc.insert(i, xm1) if pbc: rc.append(rc[0] + 1.0) else: raise ValueError(f"Wrong {order=}") return np.array(rc)
[docs] def kmesh_from_mpdivs(mpdivs, shifts, pbc=False, order="bz"): """ Returns a |numpy-array| with the reduced coordinates of the k-points from the MP divisions and the shifts. Args: mpdivs: The three MP divisions. shifts: Array-like object with the MP shift. pbc: If True, periodic images of the k-points will be included i.e. closed mesh. order: "unit_cell" if the kpoint coordinates must be in [0, 1) "bz" if the kpoint coordinates must be in [-1/2, +1/2) """ shifts = np.reshape(shifts, (-1, 3)) assert np.all(np.abs(shifts) <= 0.5) # Build k-point grid. kbz = collections.deque() for ish, shift in enumerate(shifts): rc0 = rc_list(mpdivs[0], shift[0], pbc=pbc, order=order) rc1 = rc_list(mpdivs[1], shift[1], pbc=pbc, order=order) rc2 = rc_list(mpdivs[2], shift[2], pbc=pbc, order=order) # NB: z is the fastest index in (x, y, z) for kxyz in product(rc0, rc1, rc2): #print("kxyz", kxyz) kbz.append(kxyz) #import sys; sys.exit(1) return np.array(kbz)
def map_grid2ibz(structure, ibz, ngkpt, shifts, has_timrev, pbc=False): """ Compute the correspondence between a *grid* of k-points in the *unit cell* associated to the ``ngkpt`` mesh and the corresponding points in the IBZ. Requires structure with Abinit symmetries. Args: structure: Structure with Abinit symmetry operations. ibz: [*, 3] array with the reduced coordinates in the IBZ. ngkpt: Mesh divisions. shifts: has_timrev: True if time-reversal can be used. pbc: True if the mesh should contain the periodic images (closed mesh). Returns: bz2ibz: 1d array with BZ --> IBZ mapping """ if (abispg := structure.abi_spacegroup) is None: raise ValueError("Structure does not contain Abinit spacegroup info!") ngkpt = np.asarray(ngkpt, dtype=int) shifts = np.reshape(shifts, (-1, 3)) # TODO: Handle multiple shifts if np.any(np.abs(shifts) > 0): raise ValueError("The k-mesh should be gamma-centered!") if len(shifts) > 1: raise ValueError("Multiple shifts are not supported!") # Extract rotations in reciprocal space (FM part). symrec_fm = [o.rot_g for o in abispg.fm_symmops] bzgrid2ibz = -np.ones(ngkpt, dtype=int) for ik_ibz, kibz in enumerate(ibz): gp_ibz = np.array(np.rint(kibz * ngkpt), dtype=int) for rot in symrec_fm: # Compute S k_ibz. rot_gp = np.matmul(rot, gp_ibz) gp_bz = rot_gp % ngkpt bzgrid2ibz[gp_bz[0], gp_bz[1], gp_bz[2]] = ik_ibz if has_timrev: # Compute TS k_ibz. gp_bz = (-rot_gp) % ngkpt bzgrid2ibz[gp_bz[0], gp_bz[1], gp_bz[2]] = ik_ibz if pbc: # Add periodic replicas. bzgrid2ibz = add_periodic_replicas(bzgrid2ibz) # Consistency check. if np.any(bzgrid2ibz == -1): #for ik_bz, ik_ibz in enumerate(self.bzgrid2ibz): print(ik_bz, ">>>", ik_ibz) msg = " Found %s/%s invalid entries in bzgrid2ibz array\n" % ((bzgrid2ibz == -1).sum(), bzgrid2ibz.size) msg += " This can happen if there is an inconsistency between the input IBZ and ngkpt\n" msg += " ngkpt: %s, has_timrev: %s\n" % (str(ngkpt), has_timrev) msg += f" {abispg=}\n" raise ValueError(msg) # Generate k-points using numpy's meshgrid and stack for efficiency nx, ny, nz = ngkpt[0], ngkpt[1], ngkpt[2] if pbc: nx, ny, nz = nx + 1, ny + 1, nz + 1 kx = (np.arange(nx) + shifts[0,0]) / ngkpt[0] ky = (np.arange(ny) + shifts[0,1]) / ngkpt[1] kz = (np.arange(nz) + shifts[0,2]) / ngkpt[2] # Create the 3D grid of points kx, ky, kz = np.meshgrid(kx, ky, kz, indexing="ij") bz_kpoints = np.stack((kx.ravel(), ky.ravel(), kz.ravel()), axis=-1) return bzgrid2ibz.flatten(), bz_kpoints def has_timrev_from_kptopt(kptopt): """ True if time-reversal symmetry can be used to generate k-points in the IBZ. """ # note: We assume TR if negative value i.e. band structure k-sampling. return int(kptopt) not in (3, 4) def kptopt2str(kptopt, verbose=0): """ Return human-readable string with meaning of kptopt. """ if kptopt < 0: t = ("Band structure run. Use kptbounds, and ndivk (ndivsm)" "The absolute value of kptopt gives the number of segments of the band structure." + "Weights are usually irrelevant with this option") else: t = { 0: ("Manual mode", "User-provided nkpt, kpt, kptnrm and wtk"), 1: ("Use space group symmetries and TR symmetry", "Usual mode for GS calculations (ngkpt or kptrlatt, nshiftk and shiftk)"), 2: ("Only TR symmetry", "This is to be used for DFPT at Gamma (ngkpt or kptrlatt, nshiftk and shiftk)"), 3: ("Do not take into account any symmetry", "This is to be used for DFPT at non-zero q (ngkpt or kptrlatt, nshiftk and shiftk)."), 4: ("Spatial symmetries, NO TR symmetry", "This has to be used for PAW calculations with SOC (pawspnorb/=0) " + "from ngkpt or kptrlatt, nshiftk and shiftk."), }[kptopt] return t[0] if verbose == 0 else t[0] + "\n" + t[1] def map_kpoints(other_kpoints, other_lattice, ref_lattice, ref_kpoints, ref_symrecs, has_timrev): """ Build mapping between a list of k-points in reduced coordinates (``other_kpoints``) in the reciprocal lattice ``other_lattice`` and a list of reference k-points given in the reciprocal lattice `ref_lattice` with symmetry operations ``ref_symrecs``. Args: other_kpoints: other_lattice: matrix whose rows are the reciprocal lattice vectors in cartesian coordinates. ref_lattice: same meaning as other_lattice. ref_kpoints: ref_symrecs: [nsym,3,3] arrays with symmetry operations in the `ref_lattice` reciprocal space. has_timrev: True if time-reversal can be used. Returns (o2r_map, nmissing) nmissing: Number of k-points in ref_kpoints that cannot be mapped onto ref_kpoints. o2r_map[i] gives the mapping between the i-th k-point in other_kpoints and ref_kpoints. Set to None if the i-th k-point does not have any image in ref. Each entry is a named tuple with the following attributes: ik_ref: tsign: isym g0 kpt_other = TS kpt_ref + G0 """ ref_gprimd_inv = np.inv(np.asarray(ref_lattice).T) other_gprimd = np.asarray(other_lattice).T other_kpoints = np.asarray(other_kpoints).reshape((-1, 3)) ref_kpoints = np.asarray(ref_kpoints).reshape((-1, 3)) o2r_map = len(other_kpoints) * [None] tsigns = (1, -1) if has_timrev else (1,) kmap = collections.namedtuple("kmap", "ik_ref, tsign, isym, g0") for ik_oth, okpt in enumerate(other_kpoints): # Get other k-point in reduced coordinates in the reference lattice. okpt_red = np.matmul(ref_gprimd_inv, np.matmul(other_gprimd, okpt)) # k_other = TS k_ref + G0 found = False for ik_ref, kref in enumerate(ref_kpoints): if found: break for tsign in tsigns: for isym, symrec in enumerate(ref_symrecs): krot = tsign * np.matmul(symrec, kref) if issamek(okpt_red, krot): g0 = np.rint(okpt_red - krot) o2r_map[ik_oth] = kmap(ik_ref, tsign, isym, g0) found = True break return o2r_map, o2r_map.count(None) #def find_irred_kpoints_kmesh(structure, kfrac_coords): # """ # Remove k-points that are connected to each other by one of the # symmetry operations of the space group. Assume k-points # belonging to a homogeneous mesh. # # Args: # structure: Structure object. # kfrac_coords: Reduced coordinates of the k-points. # # Return: # """ # # Wrap in [0,1[ interval. # uc_kcoords = np.reshape(kfrac_coords, (-1, 3)) % 1 # numk = len(uc_kcoords) # nx, ny, nz = np.int(np.floor(1 / uc_kcoords.min(axis=0))) # # # Compute rank and invrank # rank = np.array(numk, dtype=np.int) # invrank = {} # for ik, kk in enumerate(uc_kcoords): # rk = iz + iy * nz + ix * ny * nz # rank[ik] = rk # invrank[rank] = ik # # irred_map = collections.deque() # irred_map.append(0) # kpts2irred = collections.deque() # kpts2irred.append((0, 0, +1)) # # for ik, kk in enumerate(uc_kcoords[1:]): # ik += 1 # found = False # for ik_irr in irred_map: # kirr = kfrac_coords[ik_irr] # for isym, symmop in enumerate(structure.abi_spacegroup): # krot = symmop.rotate_k(kirr) # new_frac_coords = krot.frac_coords % 1 # if issamek(krot, kk): # #kpts2irred[ik] = ik_irr # #kpts2irred[ik] = isym # found = True # break # # #return irred_map def kpoints_indices(frac_coords, ngkpt, check_mesh=0) -> np.ndarray: """ This function is used when we need to insert k-dependent quantities in a (nx, ny, nz) array. It computes the indices of the k-points assuming these points belong to a mesh with ngkpt divisions. Args: frac_coords ngkpt: check_mesh: """ # Transforms kpt in its corresponding reduced number in the interval [0,1[ k_indices = [np.round((kpt % 1) * ngkpt) for kpt in frac_coords] k_indices = np.array(k_indices, dtype=int) # Debug secction. if check_mesh: print(f"kpoints_indices: Testing whether k-points belong to the {ngkpt =} mesh") ierr = 0 for kpt, inds in zip(frac_coords, k_indices): if check_mesh > 1: print("kpt:", kpt, "inds:", inds) same_k = np.array((inds[0]/ngkpt[0], inds[1]/ngkpt[1], inds[2]/ngkpt[2])) if not issamek(kpt, same_k): ierr += 1; print(kpt, "-->", same_k) if ierr: raise ValueError("Wrong mapping") #for kpt, inds in zip(frac_coords, k_indices): # if np.any(inds >= ngkpt): # raise ValueError(f"inds >= nkgpt for {kpt=}, {np.round(kpt % 1)=} {inds=})") print("Check succesfull!") return k_indices def find_irred_kpoints_generic(structure, kfrac_coords, verbose=1): """ Remove the k-points that are connected to each other by one of the symmetry operations of the space group. No assumption is done on the initial k-point sampling, this means that one can call this function to treat points on a path in reciprocal space. Args: structure: |Structure| object. kfrac_coords: Reduced coordinates of the k-points. Return: irred_map: Index of the i-th irreducible k-point in the input kfrac_coords array. .. warning:: In the worst case, the algorithm scales as nkpt ** 2 * nsym. hence this routine should be used only if ``kfrac_coords`` represents e.g. a path in the Brillouin zone or an arbitrary set of points. """ start = time.time() print("Removing redundant k-points. This is gonna take a while... ") # Wrap points in [0,1[ interval. uc_kcoords = np.reshape(kfrac_coords, (-1, 3)) % 1 irred_map = collections.deque() irred_map.append(0) kpts2irred = collections.deque() kpts2irred.append((0, 0, +1)) for ik, kk in enumerate(uc_kcoords[1:]): ik += 1 found = False for ik_irr in irred_map: kirr = kfrac_coords[ik_irr] for isym, symmop in enumerate(structure.abi_spacegroup): krot = symmop.rotate_k(kirr) if issamek(krot, kk): found = True #kpts2irred[ik] = (ik_irr, isym, symmops.time_sign) break if not found: irred_map.append(ik) print("Completed in", time.time() - start, "[s]") if verbose: print("Entered with ", len(uc_kcoords), "k-points") print("Found ", len(irred_map), "irred k-points") return dict2namedtuple(irred_map=np.array(irred_map, dtype=int)) def kpath_from_bounds_and_ndivsm(bounds, ndivsm, structure): """ Generate a normalized path given the extrema and the number of divisions for the smallest segment Args: bounds: (N, 3) array with the boundaries of the path in reduced coordinates. ndivsm: Number of divisions used for the smallest segment. Return: Array (M, 3) with fractional coordinates. """ bounds = np.reshape(bounds, (-1, 3)) nbounds = len(bounds) if nbounds == 1: raise ValueError("Need at least two points to define a k-path!") lens = [] for i in range(nbounds - 1): v = bounds[i + 1] - bounds[i] lens.append(float(structure.reciprocal_lattice.norm(v))) # Avoid division by zero if any bounds[i+1] == bounds[i] minlen = np.min(lens) if minlen < 1e-6: raise ValueError("Found two equivalent consecutive points in bounds!") minlen = minlen / ndivsm ndivs = np.rint(lens / minlen).astype(int) path = [] for i in range(nbounds - 1): for j in range(ndivs[i]): p = bounds[i] + j * (bounds[i + 1] - bounds[i]) / ndivs[i] path.append(p) path.append(bounds[-1]) return np.array(path) class KpointsError(Exception): """Base error class for KpointList exceptions."""
[docs] def as_kpoints(obj, lattice, weights=None, names=None): """ Convert obj into a list of k-points. Args: obj: :class:`Kpoint` or list of Kpoint objects or array-like object. lattice: Reciprocal lattice. weights: k-point weights. Ignored if obj is already a `Kpoint` instance or a list of `Kpoint` items. name: string with the name of the k-point. Ignored if obj is already a `Kpoint` instance or a list of `Kpoint` items. """ # K-point? if isinstance(obj, Kpoint): return [obj] # Iterable with K-points? if isinstance(obj, collections.abc.Iterable): if isinstance(obj[0], Kpoint): assert all(isinstance(o, Kpoint) for o in obj) return obj # Assume array-like obj = np.reshape(np.asarray(obj), (-1, 3)) ndim = obj.ndim if ndim == 1: return [Kpoint(obj, lattice, weight=weights, name=names)] elif ndim == 2: nk = len(obj) if weights is None: weights = nk * [None] if names is None: names = nk * [None] return [Kpoint(rc, lattice, weight=w, name=l) for (rc, w, l) in zip(obj, weights, names)] raise ValueError(f"{ndim=} > 2 is not supported!")
[docs] class Kpoint(SlotPickleMixin): """ Class defining one k-point. This object is immutable and can be used as key in dictionaries Note that we usually construct the object by passing pymatgen.reciprocal_lattice that is the standard reciprocal lattice used for solid state physics with a factor of 2 * pi i.e. a_i . b_j = 2pi delta_ij. Abinit, on the contrary, uses the crystallographic reciprocal lattice i.e. no 2pi factor. so pay attention when converting Abinit routines to AbiPy. """ __slots__ = [ "_frac_coords", "_lattice", "_weight", "_name", "_hash", ]
[docs] @classmethod def from_name_and_structure(cls, name, structure): """ Build Kpoint object from string with name and structure. """ frac_coords = structure.get_kcoords_from_names(name) frac_coords = np.reshape(frac_coords, (3,)) return cls(frac_coords, structure.reciprocal_lattice, weight=None, name=name)
def __init__(self, frac_coords, lattice, weight=None, name=None): """ Args: frac_coords: Reduced coordinates of the k-point. lattice: |Lattice| object describing the reciprocal lattice. weights: k-point weight (optional, set to zero if not given). name: string with the name of the k-point (optional) """ self._frac_coords = np.asarray(frac_coords) if len(self.frac_coords) != 3: raise TypeError("Expecting vector with 3 items, got `%s`" % str(self.frac_coords)) self._lattice = lattice self.set_weight(weight) self.set_name(name) def __hash__(self): """ Kpoint objects can be used as keys in dictionaries. .. warning:: The hash is computed from the fractional coordinates (floats). Hence one should avoid using hashes for implementing search algorithms in which new Kpoints are, for example generated by means of symmetry operations. This means that a dict of Kpoint objects is safe to use only when we are sure than we are going to access its entries with the *same* keys used to generate the dict!. """ try: return self._hash except AttributeError: self._hash = hash(tuple(wrap_to_ws(self.frac_coords))) return self._hash @property def frac_coords(self): """Fractional coordinates of the k-points.""" return self._frac_coords @property def lattice(self): """|Lattice| object describing the Reciprocal lattice.""" return self._lattice @property def weight(self): """Weight of the k-point. 0.0 if the weight is not defined.""" if self._weight is None: return 0.0 else: return self._weight
[docs] def set_weight(self, weight): """Set the weight of the k-point.""" self._weight = weight
[docs] @lazy_property def cart_coords(self): """Cartesian coordinates of the k-point.""" return self.lattice.get_cartesian_coords(self.frac_coords)
@property def name(self): """Name of the k-point. None if not available.""" return self._name
[docs] def set_name(self, name): """Set the name of the k-point.""" # Fix typo in Latex syntax (if any). if name is not None and name.startswith("\\"): name = "$" + name + "$" self._name = name
[docs] @lazy_property def on_border(self): """ True if the k-point is on the border of the BZ (lattice translations are taken into account). """ kreds = wrap_to_ws(self.frac_coords) diff = np.abs(np.abs(kreds) - 0.5) return np.any(diff < _ATOL_KDIFF)
def __repr__(self): s = "[%+.3f, %+.3f, %+.3f]" % tuple(self.frac_coords) if self.name is not None: s = "%s %s" % (self.name, s) return s
[docs] def tos(self, m="fract", scale=False): """ Return string with fractional or cartesian coords depending on mode `m` in ("fract", "cart", "fracart") """ def rescale(vec): #return vec if not scale: return vec vec = np.array(vec) abs_dens = np.abs(np.array([v for v in vec if v != 0.0])) if len(abs_dens) == 0: return vec d = min(v for v in abs_dens if v != 0) outs = vec / d #print("outs", outs, np.all(np.mod(outs, 1) == 0)) if np.all(np.mod(outs, 1) == 0): return outs return vec if m == "fract": return "[%.3f, %.3f, %.3f]" % tuple(rescale(self.frac_coords)) elif m == "cart": return "(%.3f, %.3f, %.3f)" % tuple(rescale(self.cart_coords)) elif m == "fracart": return "%s, %s" % (self.tos(m="fract", scale=scale), self.tos(m="cart", scale=scale)) else: raise ValueError(f"Invalid mode: {m}")
def __str__(self) -> str: return self.to_string()
[docs] def to_string(self, verbose: int = 0) -> str: """String representation.""" if verbose == 0 : s = "[%+.3f, %+.3f, %+.3f]" % tuple(self.frac_coords) elif verbose == 1: s = "[%+.6f, %+.6f, %+.6f]" % tuple(self.frac_coords) else: s = "[%+.9f, %+.9f, %+.9f]" % tuple(self.frac_coords) if self.name is not None: s = "%s %s" % (self.name, s) if self._weight is not None and float(self._weight) > 0.0: s += ", weight: %.3f" % self.weight return s
# Kpoint algebra. def __add__(self, other): return self.__class__(self.frac_coords + other.frac_coords, self.lattice) def __sub__(self, other): return self.__class__(self.frac_coords - other.frac_coords, self.lattice) def __eq__(self, other): if hasattr(other, "frac_coords"): # Comparison between two Kpoint objects return issamek(self.frac_coords, other.frac_coords) else: # Kpoint vs iterable (e.g. list) return issamek(self.frac_coords, other) def __ne__(self, other): return not (self == other) def __getitem__(self, slice): return self.frac_coords[slice]
[docs] @classmethod def as_kpoint(cls, obj, lattice): """ Convert obj into a :class:`Kpoint` instance. Args: obj: :class:`Kpoint` instance or array-like with the reduced coordinates. lattice: |Lattice| object defining the reciprocal lattice. """ if isinstance(obj, cls): return obj else: return cls(obj, lattice, weight=None, name=None)
[docs] @classmethod def gamma(cls, lattice, weight=None): """Constructor for the Gamma point.""" return cls(np.zeros(3), lattice, weight=weight, name=r"$\Gamma$")
[docs] def copy(self): """Deep copy.""" return self.__class__(self.frac_coords.copy(), self.lattice.copy(), weight=self.weight, name=self.name)
[docs] def is_gamma(self, allow_umklapp=False, atol=None) -> bool: """ Return True if this the gamma point. Args: allow_umklapp: True if umklapp G-vectors are allowed. atol: Absolute tolerance for k-point comparison (used only if umklapp). """ if not allow_umklapp: return np.all(self.frac_coords == 0.0) else: return issamek(self.frac_coords, [0, 0, 0], atol=atol)
[docs] @lazy_property def norm(self): """Norm of the kpoint.""" return np.sqrt(np.dot(self.cart_coords, self.cart_coords))
[docs] def versor(self): """Returns the versor i.e. math:`||k|| = 1`""" cls = self.__class__ if self.norm > 1e-12: return cls(self.frac_coords / self.norm, self.lattice, weight=self.weight) else: return cls.gamma(self.lattice, weight=self.weight)
[docs] def wrap_to_ws(self): """Returns a new |Kpoint| in the Wigner-Seitz zone.""" return self.__class__(wrap_to_ws(self.frac_coords), self.lattice, name=self.name, weight=self.weight)
[docs] def wrap_to_bz(self): """Returns a new |Kpoint| in the first unit cell.""" return self.__class__(wrap_to_bz(self.frac_coords), self.lattice, name=self.name, weight=self.weight)
[docs] def compute_star(self, symmops, wrap_tows=True) -> KpointStar: """Return the star of the kpoint (tuple of |Kpoint| objects).""" frac_coords = [self.frac_coords] # TODO: This part becomes a bottleneck for large nk! for sym in symmops: sk_coords = sym.rotate_k(self.frac_coords, wrap_tows=wrap_tows) # Add it only if it's not already in the list. for prev_coords in frac_coords: if issamek(sk_coords, prev_coords): break else: frac_coords.append(sk_coords) return KpointStar(self.lattice, frac_coords, weights=None, names=len(frac_coords) * [self.name])
[docs] class KpointList(collections.abc.Sequence): """ Base class defining a sequence of |Kpoint| objects. Essentially consists of base methods implementing the sequence protocol and helper functions. The subclasses |Kpath| and |IrredZone| provide specialized methods to operate on k-points representing a path or list of points in the IBZ, respectively. This object is immutable. .. Note: Algorithms usually need to know what kind of sampling we are using. The test can be easily implemented with: if kpoints.is_path: # code specific to k-paths. elif kpoints.is_ibz: # code specific to IBZ sampling. .. rubric:: Inheritance Diagram .. inheritance-diagram:: KpointList """ Error = KpointsError
[docs] @classmethod def subclass_from_name(cls, name: str): """Return the class with the given name.""" if cls.__name__ == name: return cls for c in cls.__subclasses__(): if c.__name__ == name: return c raise ValueError(f"Cannot find subclass associated to {name=}")
[docs] @classmethod def from_dict(cls, d: dict): """ Makes Kpoints obey the general json interface used in pymatgen for easier serialization. """ from pymatgen.core.lattice import Lattice reciprocal_lattice = Lattice.from_dict(d["reciprocal_lattice"]) return cls(reciprocal_lattice, d["frac_coords"], weights=d["weights"], names=d["names"], ksampling=d["ksampling"])
[docs] @pmg_serialize def as_dict(self): """ Makes Kpoints obey the general json interface used in pymatgen for easier serialization. """ if self.weights is not None: weights = self.weights.tolist() return dict( reciprocal_lattice=self.reciprocal_lattice.as_dict(), frac_coords=self.frac_coords.tolist(), weights=weights, names=[k.name for k in self], ksampling=self.ksampling, )
def __init__(self, reciprocal_lattice, frac_coords, weights=None, names=None, ksampling=None): """ Args: reciprocal_lattice: |Lattice| object. frac_coords: Array-like object with the reduced coordinates of the k-points. weights: List of k-point weights. If None, weights are initialized with zeros. names: List of k-point names. ksampling: Info on the k-point sampling (used for homogeneous meshes) """ self._reciprocal_lattice = reciprocal_lattice self._frac_coords = frac_coords = np.reshape(frac_coords, (-1, 3)) self.ksampling = ksampling if weights is not None: if len(weights) != len(frac_coords): raise ValueError("len(weights) != len(frac_coords):\nweights: %s\nfrac_coords: %s" % (weights, frac_coords)) weights = np.asarray(weights) else: weights = np.zeros(len(self.frac_coords)) if names is not None and len(names) != len(frac_coords): raise ValueError("len(names) != len(frac_coords):\nnames: %s\nfrac_coords: %s" % (names, frac_coords)) self._points = [] for i, rcs in enumerate(frac_coords): name = None if names is None else names[i] self._points.append(Kpoint(rcs, self.reciprocal_lattice, weight=weights[i], name=name)) @property def reciprocal_lattice(self): """|Lattice| object defining the reciprocal lattice.""" return self._reciprocal_lattice def __str__(self) -> str: return self.to_string()
[docs] def to_string(self, title=None, verbose=0, pre_string="") -> str: """String representation.""" lines = []; app = lines.append if title is not None: app(marquee(title, mark="=")) func = str lines.extend(["%s%d) %s" % (pre_string, i, func(kpoint)) for i, kpoint in enumerate(self)]) return "\n".join(lines)
# Sequence protocol. def __len__(self): return len(self._points) def __iter__(self): return self._points.__iter__() def __getitem__(self, slice): return self._points[slice] def __contains__(self, kpoint): return kpoint in self._points def __reversed__(self): return self._points.__reversed__() def __add__(self, other): if self.reciprocal_lattice != other.reciprocal_lattice: raise ValueError("Cannot merge k-points with different reciprocal lattices.") return KpointList(self.reciprocal_lattice, frac_coords=[k.frac_coords for k in self] + [k.frac_coords for k in other], weights=None, names=[k.name for k in self] + [k.name for k in other], ) def __eq__(self, other): if other is None or not isinstance(other, KpointList): return False for k1, k2 in zip(self, other): if k1 != k2: return False return True def __ne__(self, other): return not (self == other)
[docs] def index(self, kpoint) -> int: """ Returns: the first index of kpoint in self. Raises: `ValueError` if not found. """ try: return self._points.index(kpoint) except ValueError: raise ValueError("Cannot find point: %s in KpointList:\n%s" % (repr(kpoint), repr(self)))
[docs] def get_all_kindices(self, kpoint): """ Return numpy array with indexes of all the k-point Accepts: |Kpoint| instance or integer. """ start = self.index(kpoint) k0 = self[start] kinds = [] for ik, k in enumerate(self): if k == k0: kinds.append(ik) return np.array(kinds)
[docs] def find(self, kpoint) -> int: """ Returns: first index of kpoint. -1 if not found """ try: return self.index(kpoint) except ValueError: return -1
[docs] def count(self, kpoint): """Return number of occurrences of kpoint""" return self._points.count(kpoint)
[docs] def find_closest(self, obj): """ Find the closest k-point in the list (not necessarily equal). Args: obj: Fractional coordinates or |Kpoint| instance. Return: (ind, kpoint, dist) where ``ind`` is the index in self of the closest k-point. ``kpoint`` is the |Kpoint| instance of index ``ind``. dist is the distance between ``obj`` and ``kpoint``. """ if isinstance(obj, Kpoint): if obj.lattice != self.reciprocal_lattice: raise ValueError("Kpoint list and Kpoint object have different lattices!") frac_coords = obj.frac_coords else: frac_coords = np.asarray(obj) dist = np.empty(len(self)) for i, kpt in enumerate(self): dist[i] = float(kpt.lattice.norm(kpt.frac_coords - frac_coords)) ind = dist.argmin() return ind, self[ind], np.copy(dist[ind])
@property def is_path(self) -> bool: """True if self represents a path in the BZ.""" return isinstance(self, Kpath) @property def is_ibz(self) -> bool: """True if self represents a list of points in the IBZ.""" return isinstance(self, IrredZone)
[docs] @lazy_property def mpdivs_shifts(self): """ The Monkhorst-Pack (MP) divisions and shifts. Both quantities are set to None if self is not a MP mesh. Use `is_mpmesh` to check whether self is a MP mesh. """ if not self.is_ibz: return (None, None) # Test if kptrlatt is diagonal. if not is_diagonal(self.ksampling.kptrlatt): return (None, None) return self.ksampling.kptrlatt.diagonal(), self.ksampling.shifts
@property def is_mpmesh(self) -> bool: """ True if self represents a Monkhorst-Pack mesh. i.e if the sampling has been specified in terms of divisions along the reciprocal lattice vectors (ngkpt) """ return self.mpdivs_shifts[0] is not None @property def frac_coords(self): """Fractional coordinates of the k-point as |numpy-array| of shape (len(self), 3)""" return self._frac_coords
[docs] def get_cart_coords(self): """Cartesian coordinates of the k-point as |numpy-array| of shape (len(self), 3)""" return np.reshape([k.cart_coords for k in self], (-1, 3))
@property def names(self) -> list[str]: """List with the name of the k-points.""" return [k.name for k in self] @property def weights(self) -> np.ndarray: """|numpy-array| with the weights of the k-points.""" return np.array([kpoint.weight for kpoint in self])
[docs] def sum_weights(self): """Returns the sum of the weights.""" return np.sum(self.weights)
[docs] def check_weights(self) -> None: """ Check if weights are normalized to one. Raises: `ValueError` if Weights are not normalized. """ # Weights must be normalized to one. wsum = self.sum_weights() if abs(wsum - 1) > 1.e-6: err_msg = "Kpoint weights should sum up to one while sum_weights is %.3f\n" % wsum err_msg += "The list of kpoints does not represent a homogeneous sampling of the BZ\n" err_msg += "%s\n%s" % (self.__class__, self.to_string(verbose=0)) raise ValueError(err_msg)
[docs] def get_highsym_datataframe(self, with_cart_coords=False): """ Return pandas Dataframe with the names of the high-symmetry k-points and the reduced coordinates. Args: with_cart_coords: True to add extra column with the Cartesian coordinates. Return: |pandas-DataFrame| """ replace = { r"$\Gamma$": "Γ", } import pandas as pd rows, index = [], [] for ik, kpt in enumerate(self): if kpt.name is None: continue d = dict(name=replace.get(kpt.name, kpt.name), frac_coords=kpt.frac_coords) if with_cart_coords: d["cart_coords"] = kpt.cart_coords rows.append(d) index.append(ik) df = pd.DataFrame(rows, index=index) df.index.name = "Idx" return df
[docs] def remove_duplicated(self): """ Remove duplicated k-points from self. Returns new :class:`KpointList` instance. """ frac_coords, good_indices = [self[0].frac_coords], [0] for i, kpoint in enumerate(self[1:]): i += 1 # Add it only if it's not already in the list. for prev_coords in frac_coords: if issamek(kpoint.frac_coords, prev_coords): break else: frac_coords.append(kpoint.frac_coords) good_indices.append(i) good_kpoints = [self[i] for i in good_indices] return self.__class__( self.reciprocal_lattice, frac_coords=[k.frac_coords for k in good_kpoints], weights=None, names=[k.name for k in good_kpoints], ksampling=self.ksampling)
[docs] def to_array(self): """Returns a |numpy-array| [nkpy, 3] with the fractional coordinates.""" return np.array(self.frac_coords.copy())
[docs] def to_json(self) -> str: """ Returns a JSON_ string representation of the MSONable object. """ from monty.json import MontyEncoder return json.dumps(self.as_dict(), cls=MontyEncoder)
[docs] def plot(self, ax=None, **kwargs): """Plot k-points with matplotlib.""" from pymatgen.electronic_structure.plotter import plot_brillouin_zone fold = False if self.is_path: labels = {k.name: k.frac_coords for k in self if k.name} frac_coords_lines = [self.frac_coords[line] for line in self.lines] return plot_brillouin_zone(self.reciprocal_lattice, lines=frac_coords_lines, labels=labels, ax=ax, fold=fold, **kwargs) else: # Not sure this works, I got points outside of the BZ in a simple with Si and Gamma-centered 8x8x8. # Don't know if it's a bug in matplotlib or plot_brillouin_zone. #print(self.frac_coords) return plot_brillouin_zone(self.reciprocal_lattice, kpoints=self.frac_coords, ax=ax, fold=fold, **kwargs)
[docs] def plotly(self, fig=None, **kwargs): """Plot k-points with plotly.""" from abipy.tools.plotting import plotly_brillouin_zone fold = False if self.is_path: labels = {k.name: k.frac_coords for k in self if k.name} frac_coords_lines = [self.frac_coords[line] for line in self.lines] return plotly_brillouin_zone(self.reciprocal_lattice, lines=frac_coords_lines, labels=labels, fig=fig, fold=fold, **kwargs) else: return plotly_brillouin_zone(self.reciprocal_lattice, kpoints=self.frac_coords, fig=fig, fold=fold, **kwargs)
[docs] def get_k2kqg_map(self, qpt, atol_kdiff=None): """ Compute mapping k_index --> (k + q)_index, g0 Args: qpt: q-point in fractional coordinate or :class:`Kpoint` instance. atol_kdiff: Tolerance used to compare k-points. Use _ATOL_KDIFF is atol is None. """ if atol_kdiff is None: atol_kdiff = _ATOL_KDIFF if isinstance(qpt, Kpoint): qfrac_coords = qpt.frac_coords else: qfrac_coords = np.reshape(qpt, (3,)) k2kqg = collections.OrderedDict() if np.all(np.abs(qfrac_coords) <= 1e-6): # Gamma point, DOH! g0 = np.zeros(3, dtype=int) for ik, _ in enumerate(self): k2kqg[ik] = (ik, g0) else: # N**2 scaling but this algorithm can handle k-paths # Note that in principle one could have multiple k+q in k-points # but only the first match is considered. for ik, kpoint in enumerate(self): kpq = kpoint.frac_coords + qfrac_coords for ikq, ksearch in enumerate(self): if issamek(kpq, ksearch.frac_coords, atol=atol_kdiff): g0 = np.rint(kpq - ksearch.frac_coords) k2kqg[ik] = (ikq, g0) break return k2kqg
[docs] class KpointStar(KpointList): """ Star of the kpoint. Note that the first k-point is assumed to be the base of the star namely the point that is used to generate the Star. .. inheritance-diagram:: KpointStar """ @property def base_point(self): """The point used to generate the star.""" return self[0] @property def name(self): """The name of the star (inherited from the name of the base point).""" return self.base_point.name
[docs] class Kpath(KpointList): """ This object describes a k-path in reciprocal space. It provides methods to compute (line) derivatives along the path. The k-points do not have weights so Kpath should not be used to compute integral in the BZ. .. inheritance-diagram:: Kpath """
[docs] @classmethod def from_names(cls, structure, knames, line_density=20): """ Generate normalized K-path from list of k-point labels. Args: structure: |Structure| object. knames: List of strings with the k-point labels. line_density: Number of points used to sample the smallest segment of the path """ kfrac_coords = structure.get_kcoords_from_names(knames) vertices_names = list(zip(kfrac_coords, knames)) return cls.from_vertices_and_names(structure, vertices_names, line_density=line_density)
[docs] @classmethod def from_vertices_and_names(cls, structure, vertices_names, line_density=20): """ Generate normalized k-path from a list of vertices and the corresponding labels. Args: structure: |Structure| object. vertices_names: List of tuple, each tuple is of the form (kfrac_coords, kname) where kfrac_coords are the reduced coordinates of the k-point and kname is a string with the name of the k-point. Each point represents a vertex of the k-path. line_density: Number of points used to sample the smallest segment of the path. If 0, use list of k-points given in vertices_names """ if line_density == 0: frac_coords = [vn[0] for vn in vertices_names] knames = [vn[1] for vn in vertices_names] return cls(structure.lattice.reciprocal_lattice, frac_coords=frac_coords, weights=None, names=knames) gmet = structure.lattice.reciprocal_lattice.metric_tensor vnames = [str(vn[1]) for vn in vertices_names] vertices = np.array([vn[0] for vn in vertices_names], dtype=float) vertices.shape = (-1, 3) dl_vals = [] for ik, k0 in enumerate(vertices[:-1]): dk = vertices[ik + 1] - k0 dl = np.sqrt(np.dot(dk, np.matmul(gmet, dk))) if abs(dl) < 1e-6: dl = np.inf dl_vals.append(dl) dl_min = np.array(dl_vals).min() fact = dl_min / line_density frac_coords = collections.deque() knames = collections.deque() for ik, dl in enumerate(dl_vals): if dl == np.inf: continue numk = int(np.rint(dl / fact)) k0 = vertices[ik] dk = vertices[ik + 1] - k0 knames.append(vnames[ik]) for ii in range(numk): next_k = k0 + dk * ii / numk frac_coords.append(next_k) if ii > 0: knames.append("") knames.append(vnames[-1]) frac_coords.append(vertices[-1]) return cls(structure.lattice.reciprocal_lattice, frac_coords=frac_coords, weights=None, names=knames)
def __str__(self): return self.to_string()
[docs] def to_string(self, verbose=0, title=None, **kwargs) -> str: """ String representation. Args: verbose: Verbosity level. Default: 0 """ lines = []; app = lines.append if title is not None: app(marquee(title, mark="=")) app("K-path contains %s lines. Number of k-points in each line: %s" % ( len(self.lines), [len(l) for l in self.lines])) if verbose: for i, line in enumerate(self.lines): app("line %d: %s" % (i, line)) header = "\n".join(lines) vids = {line[0] for line in self.lines} table = [["Idx", "Frac_coords", "Name", "ds", "Vert",]] for i, kpoint in enumerate(self): tag = "*" if i in vids else " " if verbose == 0 and not tag: continue table.append([ str(i), "%.7f, %.7f, %.7f" % tuple(kpoint.frac_coords), kpoint.name, self.ds[i] if i != len(self) - 1 else None, "*" if i in vids else " ", ]) return "\n".join([header, " ", tabulate(table, headers="firstrow")])
[docs] @lazy_property def ds(self): """ |numpy-array| of len(self)-1 elements giving the distance between two consecutive k-points, i.e. ds[i] = ||k[i+1] - k[i]|| for i=0,1,...,n-1 """ ds = np.zeros(len(self) - 1) for i, kpoint in enumerate(self[:-1]): ds[i] = (self[i + 1] - kpoint).norm return ds
[docs] @lazy_property def versors(self): """ Tuple of len(self) - 1 elements with the versors connecting k[i] to k[i+1]. """ versors = (len(self) - 1) * [None, ] for i, kpt in enumerate(self[:-1]): versors[i] = (self[i + 1] - kpt).versor() return tuple(versors)
[docs] @lazy_property def lines(self) -> list: """ Nested list containing the indices of the points belonging to the same line. Used for extracting the eigenvalues while looping over the lines. Example: for line in self.lines: vals_on_line = eigens[spin, line, band] """ if len(self) < 2: return tuple() prev = self.versors[0] lines = [[0]] for i, v in enumerate(self.versors[1:]): i += 1 #if v != prev: if ((prev - v).norm > 1e-5): #print("diff", (prev - v).norm, v.frac_coords - prev.frac_coords) prev = v lines[-1].append(i) lines.append([i]) else: lines[-1].append(i) lines[-1].append(len(self)-1) return tuple(lines)
[docs] @lazy_property def frac_bounds(self): """Numpy array of shape [M, 3] with the vertexes of the path in frac coords.""" frac_bounds = [self[line[0]].frac_coords for line in self.lines] frac_bounds.append(self[self.lines[-1][-1]].frac_coords) return np.reshape(frac_bounds, (-1, 3))
[docs] @lazy_property def cart_bounds(self): """Numpy array of shape [M, 3] with the vertexes of the path in frac coords.""" cart_bounds = [self[line[0]].cart_coords for line in self.lines] cart_bounds.append(self[self.lines[-1][-1]].cart_coords) return np.reshape(cart_bounds, (-1, 3))
[docs] def find_points_along_path(self, cart_coords, dist_tol=1e-12): """ Find points in ``cart_coords`` lying on the path with distance less than `dist_tol`. See find_points_along_path function for API. """ return find_points_along_path(self.cart_bounds, cart_coords, dist_tol=dist_tol)
[docs] def finite_diff(self, values, order=1, acc=4): """ Compute the derivatives of values by finite differences. Args: values: array-like object with shape=(nkpt) containing the values of the path. order: Order of the derivative. acc: Accuracy: 4 corresponds to a central difference with 5 points. Returns: ragged numpy array. The i-th entry is a numpy array with the derivatives on the i-th line. """ values = np.asarray(values) assert len(values) == len(self) # Loop over the lines of the path, extract the data on the line and # differentiate f(s) where s is the distance between two consecutive points along the line. ders_on_lines = [] for line in self.lines: vals_on_line = values[line] h = self.ds[line[0]] if not np.allclose(h, self.ds[line[:-1]]): raise ValueError("For finite difference derivatives, the path must be homogeneous!\n" + str(self.ds[line[:-1]])) der = finite_diff(vals_on_line, h, order=order, acc=acc) ders_on_lines.append(der) return np.array(ders_on_lines, dtype=object)
[docs] class IrredZone(KpointList): """ Immutable sequence of points in the irreducible wedge of the BZ. Each point has a weight whose sum must equal 1 so that we can integrate quantities in the full Brillouin zone. .. note:: Abinit supports different options for the specification of the BZ sampling: - kptrlatt(3,3) or ngkpt(3) for the definition grid. - shiftk(3, nshiftk) for the definition of multiple shifts. - `kptopt` for the treatment of symmetry operations. All these possibilities complicate the internal implementation in particular when we need to recostruct the full BZ and take into account the presence of multiple shifts since kptrlatt may have non-zero off-diagonal components. Client code that needs to know how the mesh was generated can rely on the following checks: if not self.ibz: raise("Need an IBZ sampling") mpdivs, shifts = self.mpdivs_shifts if mpdivs is None: raise ValueError("Cannot handle kptrlatt with non-zero off-diagonal elements") if len(shifts) > 1: raise ValueError("Multiple shifts are not supported") # Code for mesh defined in terms of mpdivs and one shift. .. inheritance-diagram:: IrredZone """
[docs] @classmethod def from_ngkpt(cls, structure, ngkpt, shiftk, kptopt=1, spin_mode="unpolarized", verbose=0) -> IrredZone: """ Build an IrredZone instance from (ngkpt, shift) by calling Abinit to get the list of IBZ k-points. """ from abipy.abio.factories import gs_input from abipy.data.hgh_pseudos import HGH_TABLE gsinp = gs_input(structure, HGH_TABLE, spin_mode=spin_mode) ibz = gsinp.abiget_ibz(ngkpt=ngkpt, shiftk=shiftk, kptopt=kptopt, verbose=verbose) ksampling = KSamplingInfo.from_mpdivs(ngkpt, shiftk, kptopt) return cls(structure.reciprocal_lattice, ibz.points, weights=ibz.weights, names=None, ksampling=ksampling)
[docs] @classmethod def from_kppa(cls, structure, kppa, shiftk, kptopt=1, verbose=0) -> IrredZone: """ Build an IrredZone instance from (kppa, shift) by calling Abinit to get the list of IBZ k-points. """ from abipy.abio.factories import gs_input from abipy.data.hgh_pseudos import HGH_TABLE gsinp = gs_input(structure, HGH_TABLE, spin_mode="unpolarized", kppa=kppa) ibz = gsinp.abiget_ibz(ngkpt=None, shiftk=shiftk, kptopt=kptopt, verbose=verbose) ksampling = KSamplingInfo.from_mpdivs(gsinp["ngkpt"], shiftk, kptopt) return cls(structure.reciprocal_lattice, ibz.points, weights=ibz.weights, names=None, ksampling=ksampling)
def __init__(self, reciprocal_lattice, frac_coords, weights=None, names=None, ksampling=None): """ Args: reciprocal_lattice: |Lattice| object frac_coords: Array-like object with the reduced coordinates of the points. weights: Array-like with the weights of the k-points. names: List with the name of the k-points. ksampling: Info on the k-point sampling """ super().__init__(reciprocal_lattice, frac_coords, weights=weights, names=names, ksampling=ksampling) # Weights must be normalized to one. wsum = self.sum_weights() if abs(wsum - 1) > 1.e-6: err_msg = ("The list of kpoints does not represent a homogeneous sampling of the BZ\n" "Kpoint weights should sum up to one while sum_weights is %.3f\n" % wsum) print(err_msg) #raise ValueError(err_msg) def __str__(self) -> str: return self.to_string()
[docs] def to_string(self, verbose: int=0, title=None) -> str: """String representation.""" lines = []; app = lines.append if title is not None: app(marquee(title, mark="=")) if self.is_mpmesh: mpdivs, shifts = self.mpdivs_shifts d = "[%d, %d, %d]" % tuple(mpdivs) s = ", ".join("[%.1f, %.1f, %.1f]" % tuple(s) for s in shifts) app("K-mesh with divisions: %s, shifts: %s" % (d, s)) app("kptopt: %s (%s)" % (self.ksampling.kptopt, kptopt2str(self.ksampling.kptopt))) else: app("nkpt: %d" % len(self)) app(self.ksampling.to_string(verbose=verbose)) app("Number of points in the IBZ: %s" % len(self)) for i, k in enumerate(self): if i > 10 and verbose == 0: app(4 * " " + "... (More than 10 k-points)") break app("%6d) [%+.3f, %+.3f, %+.3f], weight=%.3f" % (i, k[0], k[1], k[2], k.weight)) return "\n".join(lines)
class KSamplingInfo(AttrDict): """ Store metadata defining the k-point sampling according to the abinit conventions. One should pass through one of the class methods to create the class, avoid calling __init__ directly. """ KNOWN_KEYS = set([ "mpdivs", # Mesh divisions. Defined only if we have a sampling with diagonal kptrlatt else None. "kptrlatt", # [3, 3] matrix defined only if we have a sampling else None. "kptrlatt_orig", # Original set of shifts. Defined only if we have a sampling else None. "shifts", # Actual shifts (Usually one). Defined only if we have a sampling else None. "shifts_orig", # Original shifts specified by the user. Defined only if we have a sampling else None. "kptopt", # Options for k-point generation. Negative if we have a k-path (nbounds - 1). ]) @classmethod def as_ksampling(cls, obj) -> KSamplingInfo: """" Convert obj into a :class:`KSamplingInfo` instance. Accepts: :class:`KSamplingInfo` instance, None (if info are not available) or dict-like object. """ if isinstance(obj, cls): return obj if obj is None: return cls(mpdivs=None, kptrlatt=None, kptrlatt_orig=None, shifts=None, shifts_orig=None, kptopt=0, ) # Assume dict-like object. try: return cls(**obj) except Exception as exc: raise TypeError("Don't know how to convert `%s` into KSamplingInfo object:\n%s" % (type(obj), str(exc))) @classmethod def from_mpdivs(cls, mpdivs, shifts, kptopt) -> KSamplingInfo: """ Homogeneous sampling specified in terms of ``mpdivs`` (ngkpt in abinit), the set of ``shifts`` and the value of ``kptopt``. """ kptrlatt = kptrlatt_orig = np.diag(mpdivs) shifts = shifts_orig = np.reshape(np.array(shifts), (-1, 3)) return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig, kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt) @classmethod def from_kptrlatt(cls, kptrlatt, shifts, kptopt) -> KSamplingInfo: """ Homogeneous sampling specified in terms of ``kptrlatt`` the set of ``shifts`` and the value of ``kptopt``. """ kptrlatt = kptrlatt_orig = np.reshape(kptrlatt, (3, 3)) shifts = shifts_orig = np.reshape(np.array(shifts), (-1, 3)) # Test if kptrlatt is diagonal. mpdivs = None if not is_diagonal(kptrlatt) else np.diag(kptrlatt) return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig, kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt) @classmethod def from_kbounds(cls, kbounds) -> KSamplingInfo: """ Metadata associated to a k-path specified in terms of boundaries. """ mpdivs, kptrlatt, kptrlatt_orig, shifts, shifts_orig = 5 * (None,) kptopt = - (len(np.reshape(kbounds, (-1, 3))) - 1) # Note -1 return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig, kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for key in self: if key not in self.KNOWN_KEYS: raise ValueError(f"Unknown {key=}") # FIXME: monkhorst_pack_folding is not written in e.g. DEN.nc files # so we get crazy results because of netCDF4._default_fillvals # This part set the value of mpdivs from kptrlatt. if self["mpdivs"] is not None and np.any(np.abs(self["mpdivs"]) > 1e+6): if self.kptrlatt_orig is not None: # We have a sampling if np.all(self.kptrlatt_orig == self.kptrlatt) and is_diagonal(self.kptrlatt): self["mpdivs"] = np.diag(self.kptrlatt) else: self["mpdivs"] = None # import warnings # warnings.warn(""" #monkhorst_pack_folding variables has not been written to netcdf file. #Received {mpdivs} #Setting mpdivs to None, this may create problems in post-processing tools. #If needed, use python netcdf to change the value of `monkhorst_pack_folding`""".format(mpdivs=self["mpdivs"])) def __str__(self): return self.to_string() def to_string(self, verbose=0, title=None, **kwargs) -> str: """String representation.""" lines = []; app = lines.append if title is not None: app(marquee(title, mark="=")) if self.is_mesh: if self.has_diagonal_kptrlatt: app("mpdivs: %s with shifts %s and kptopt: %s" % (str(self.mpdivs), str(self.shifts), self.kptopt)) else: app("kptrlatt: %s" % str(self.kptrlatt)) app("shifts: %s" % str(self.shifts)) app("kptrlatt_orig: %s" % str(self.kptrlatt_orig)) app("shifts_orig: %s" % str(self.shifts_orig)) app("kptopt: %s (%s)" % (str(self.kptopt), kptopt2str(self.kptopt))) elif self.is_path: app("Path with kptopt: %s" % self.kptopt) else: app("Neither mesh or path!") return "\n".join(lines) @property def is_mesh(self) -> bool: """True if we have a mesh in the BZ.""" return self.kptopt > 0 and (self.mpdivs is not None or self.kptrlatt is not None) @property def is_path(self) -> bool: """True if we have a path in the BZ.""" return self.kptopt < 0 #@property #def is_homogeneous(self): # """True if we have a homogeneous sampling of the BZ.""" # return self.kptopt > 0 and (self.mpdivs is not None or self.kptrlatt is not None) @property def has_diagonal_kptrlatt(self) -> bool: """True if sampling with diagonal kptrlatt.""" if self.kptrlatt is None: return False return is_diagonal(self.kptrlatt) class KpointsReaderMixin: """ Mixin class that provides methods for reading k-point data from a netcdf file written according to the ETSF-IO specifications. """ def read_kpoints(self): """ Factory function: returns an instance of :class:`Kpath` or :class:`IrredZone` depending on the content of the Netcdf file. Main entry point for client code. """ structure = self.read_structure() frac_coords = self.read_kfrac_coords() weights = self.read_kweights() ksampling = self.read_ksampling_info() if ksampling.kptopt < 0 or np.all(weights == 1): # We have a path in the BZ. kpath = Kpath(structure.reciprocal_lattice, frac_coords, ksampling=ksampling) for kpoint in kpath: kpoint.set_name(structure.findname_in_hsym_stars(kpoint)) return kpath # FIXME # Quick and dirty hack to allow the reading of the k-points from WFK files # where info on the sampling is missing. I will regret it but at present # is the only solution I found (changes in the ETSF-IO part of Abinit are needed) #if ksampling.is_homogeneous or abs(sum(weights) - 1.0) < 1.e-6: #if np.any(ksampling.kptrlatt_orig != 0) or abs(sum(weights) - 1.0) < 1.e-6: #if np.any(ksampling.kptrlatt_orig != 0): # We have a homogeneous sampling of the BZ. return IrredZone(structure.reciprocal_lattice, frac_coords, weights=weights, ksampling=ksampling) def read_ksampling_info(self) -> KSamplingInfo: """ Read information on the k-point sampling. Return :class:`KSamplingInfo` object. """ # FIXME: in v8.0, the SIGRES files does not have kptopt, kptrlatt_orig and shiftk_orig kptrlatt = self.read_kptrlatt() shifts = self.read_kshifts() return KSamplingInfo( mpdivs=self.read_kmpdivs(), kptrlatt=kptrlatt, kptrlatt_orig=self.read_value("kptrlatt_orig", default=kptrlatt), shifts=shifts, shifts_orig=self.read_value("shiftk_orig", default=shifts), kptopt=int(self.read_value("kptopt", default=0)), ) def read_kfrac_coords(self) -> np.ndarray: """Fractional coordinates of the k-points""" return self.read_value("reduced_coordinates_of_kpoints") def read_kweights(self) -> np.ndarray: """Returns the weight of the k-points. None if not found.""" return self.read_value("kpoint_weights") def read_kshifts(self) -> np.ndarray: """Returns the shifts of the k-mesh in reduced coordinates. None if not found.""" try: return self.read_value("shiftk") except self.Error: return self.read_value("kpoint_grid_shift") def read_kmpdivs(self) -> np.ndarray: """Returns the Monkhorst-Pack divisions defining the mesh. None if not found.""" if "monkhorst_pack_folding" in self.rootgrp.variables: return self.none_if_masked_array(self.read_value("monkhorst_pack_folding")) else: kptrlatt = self.read_kptrlatt() kmpdivs = np.diag(kptrlatt) for i in range(3): for j in range(3): if i != j and kptrlatt[i, j] != 0: kmpdivs = None return kmpdivs def read_kptrlatt(self) -> np.ndarray: """Returns ABINIT variable kptrlatt. None if not found.""" try: return self.read_value("kptrlatt") except self.Error: return self.read_value("kpoint_grid_vectors") class KpointsReader(ETSF_Reader, KpointsReaderMixin): """ This object reads k-point data from a netcdf file. .. inheritance-diagram:: KpointsReader """
[docs] class Ktables: """ Call spglib to compute the k-points in the IBZ with the corresponding weights as well as the mapping BZ --> IBZ. Args: mesh: is_shift: Attributes: mesh is_shift ibz: nibz weights: bz: nbz grid: """ def __init__(self, structure, mesh, is_shift, has_timrev): """ Args: structure mesh is_shift has_timrev """ import spglib as spg self.mesh = np.array(mesh) self.is_shift = is_shift self.has_timrev = has_timrev cell = (structure.lattice.matrix, structure.frac_coords, structure.atomic_numbers) mapping, self.grid = spg.get_ir_reciprocal_mesh(self.mesh, cell, is_shift=self.is_shift, is_time_reversal=self.has_timrev, symprec=_SPGLIB_SYMPREC) uniq, self.weights = np.unique(mapping, return_counts=True) self.weights = np.asarray(self.weights, dtype=float) / len(self.grid) self.nibz = len(uniq) self.kshift = [0., 0., 0.] if is_shift is None else 0.5 * np.asarray(is_shift) self.ibz = (self.grid[uniq] + self.kshift) / self.mesh self.bz = (self.grid + self.kshift) / self.mesh self.nbz = len(self.bz) # All k-points and mapping to ir-grid points. # FIXME This is slow. self.bz2ibz = np.empty(len(self.bz), dtype=int) for ik_bz, ir_gp_id in enumerate(mapping): inds = np.where(uniq == ir_gp_id) assert len(inds) == 1 self.bz2ibz[ik_bz] = int(inds[0]) def __str__(self): return self.to_string()
[docs] def to_string(self, verbose=0, title=None, **kwargs) -> str: """String representation""" lines = collections.deque(); app = lines.append if title is not None: app(marquee(title, mark="=")) app("mesh %s, shift %s, time-reversal: %s, Irred points: %d" % ( self.mesh, self.kshift, self.has_timrev, self.nibz)) app("Irreducible k-points with number of points in star:\n") for ik, kpt in enumerate(self.ibz): app("%s: [%9.6f, %9.6f, %9.6f], nstar: %d" % (ik, kpt[0], kpt[1], kpt[2], self.weights[ik] * self.nbz)) return "\n".join(lines)
[docs] def print_bz2ibz(self, file=sys.stdout) -> None: """Print BZ --> IBZ mapping.""" print("BZ points --> IBZ points mapping", file=file) for ik_bz, ik_ibz in enumerate(self.bz2ibz): print("%6d) [%9.6f, %9.6f, %9.6f], ===> %6d) [%9.6f, %9.6f, %9.6f]," % (ik_bz, self.bz[ik_ibz][0], self.bz[ik_ibz][1], self.bz[ik_ibz][2], ik_ibz, self.ibz[ik_ibz][0], self.ibz[ik_ibz][1], self.ibz[ik_ibz][2]), file=file)
def dist_point_from_line(x0, x1, x2): """ Return distance from point x0 to line x1 - x2. Cartesian coordinates are used. See <http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html> """ denom = x2 - x1 denomabs = np.sqrt(np.dot(denom, denom)) numer = np.cross(x0 - x1, x0 - x2) numerabs = np.sqrt(np.dot(numer, numer)) return numerabs / denomabs
[docs] def find_points_along_path(cart_bounds, cart_coords, dist_tol): """ Find points in ``cart_coords`` lying on the path defined by ``cart_bounds``. Result are ordered according to distance along the path. Args: cart_bounds: [N, 3] array with the boundaries of the path in Cartesian coordinates. cart_coords: [M, 3] array with the points in Cartesian coordinate dist_tol: A point is considered to be on the path if its distance from the line is less than dist_tol. Return: namedtuple with the following attributes: (ikfound, dist_list, path_ticks) ikfound is a numpy array with the indices of the points lying on the path. Empty if no point is found. dist_list: numpy array with the distance of the points along the line. path_ticks: numpy array with the ticks associated to the input k-path. """ ikfound, dist_list, path_ticks = [], [], [0] dl = 0 # cumulative length of the path for ibound, x0 in enumerate(cart_bounds[:-1]): x1 = cart_bounds[ibound + 1] B = x0 - x1 #B = x1 - x0 dk = np.sqrt(np.dot(B,B)) #print("x0", x0, "x1", x1) path_ticks.append(path_ticks[ibound] + dk) for ik, k in enumerate(cart_coords): dist = dist_point_from_line(k, x0, x1) #print(frac_coords[ik], dist) if dist > dist_tol: continue # k-point is on the cart_bounds A = x0 - k #A = k - x0 x = np.dot(A, B)/dk #print("k-x0", A, "B", B) #print(frac_coords[ik], x, x > 0 and x < dist_tol + dk) if dist_tol + dk >= x >= 0: # k-point is within the cart_bounds range # append k-point coordinate along the cart_bounds while avoing duplicate entries. if ikfound and ik == ikfound[-1]: continue ikfound.append(ik) dist_list.append(x + dl) dl = dl + dk # Sort dist_list and ikfound by cumulative length while removing possible duplicated entries. dist_list, isort = np.unique(np.array(dist_list).round(decimals=5), return_index=True) return dict2namedtuple(ikfound=np.array(ikfound)[isort], dist_list=dist_list, path_ticks=np.array(path_ticks))
def build_segments(k0_list, npts, step, red_dirs, reciprocal_lattice) -> np.ndarray: """ For each point in k0_list, build a line passing through the point for each reduced direction in red_dir. Each line consists of `npts` points with step `step` in Ang-1 and is centered on the k-point. Return: (nk0_list, len(red_dirs) * npts, 3) array with fractional coordinates. Args: k0_list: List of k-points in reduced coordinates. npts: Number of points in each segment. step: Step in Ang-1 red_dirs: List of reduced directions reciprocal_lattice: Reciprocal lattice (from structure.reciprocal_lattice) """ k0_list = np.reshape(k0_list, (-1, 3)) red_dirs = np.reshape(red_dirs, (-1, 3)) kpts = [] for kpoint in k0_list: kpoint = Kpoint.as_kpoint(kpoint, reciprocal_lattice) # Build segments passing through this kpoint (work in Cartesian coords) for rdir in red_dirs: bvers = reciprocal_lattice.matrix.T @ rdir #bvers = reciprocal_lattice.get_cartesian_coords(rdir) bvers /= np.sqrt(np.dot(bvers, bvers)) kstart = kpoint.cart_coords - bvers * (npts // 2) * step for ii in range(npts): kpts.append(kstart + ii * step * bvers) # Cart --> Frac out = reciprocal_lattice.get_fractional_coords(kpts) out = np.reshape(out, (len(k0_list), len(red_dirs) * npts, 3)) # Set small values to zero. out = np.where(np.abs(out) > 1e-12, out, 0.0) return out