Source code for wulfric.kpoints

# Wulfric - Crystal, Lattice, Atoms, K-path.
# Copyright (C) 2023-2024 Andrey Rybakov
#
# e-mail: anry@uv.es, web: adrybakov.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from copy import deepcopy
from typing import Iterable

import numpy as np

from wulfric.geometry import absolute_to_relative

__all__ = ["Kpoints"]


[docs] class Kpoints: r""" K-point path. Parameters ---------- b1 : (3,) array-like First reciprocal lattice vector :math:`\mathbf{b_1}`. b2 : (3,) array-like Second reciprocal lattice vector :math:`\mathbf{b_2}`. b3 : (3,) array-like Third reciprocal lattice vector :math:`\mathbf{b_3}`. coordinates : list, optional Coordinates are given in relative coordinates in reciprocal space. names: list, optional Names of the high symmetry points. Used for programming, not for plotting. labels : list, optional Dictionary of the high symmetry points labels for plotting. Has to have the same length as ``coordinates``. path : str, optional K points path. n : int Number of points between each pair of the high symmetry points (high symmetry points excluded). Attributes ---------- b1 : (3,) :numpy:`ndarray` First reciprocal lattice vector :math:`\mathbf{b_1}`. b2 : (3,) :numpy:`ndarray` Second reciprocal lattice vector :math:`\mathbf{b_2}`. b3 : (3,) :numpy:`ndarray` Third reciprocal lattice vector :math:`\mathbf{b_3}`. hs_names : list Names of the high symmetry points. Used for programming, not for plotting. hs_coordinates : dict Dictionary of the high symmetry points coordinates. .. code-block:: python {"name": [k_a, k_b, k_c], ... } hs_labels : dict Dictionary of the high symmetry points labels for plotting. .. code-block:: python {"name": "label", ... } """ def __init__( self, b1, b2, b3, coordinates=None, names=None, labels=None, path=None, n=100 ) -> None: self.b1 = np.array(b1) self.b2 = np.array(b2) self.b3 = np.array(b3) if coordinates is None: coordinates = [] # Fill names and labels with defaults if names is None: names = [f"K{i+1}" for i in range(len(coordinates))] if labels is None: labels = [f"K$_{i+1}$" for i in range(len(coordinates))] if labels is None: labels = [name for name in names] else: if len(labels) != len(coordinates): raise ValueError( f"Amount of labels ({len(labels)}) does not match amount of points ({len(coordinates)})." ) # Define high symmetry points attributes self.hs_coordinates = dict( [(names[i], np.array(coordinates[i])) for i in range(len(coordinates))] ) self.hs_labels = dict([(names[i], labels[i]) for i in range(len(coordinates))]) self.hs_names = names self._n = n self._path = None if path is None: path = "-".join(self.hs_names) self.path = path ################################################################################ # High symmetry points # ################################################################################
[docs] def add_hs_point(self, name, coordinates, label, relative=True): r""" Add high symmetry point. Parameters ---------- name : str Name of the high symmetry point. coordinates : (3,) array-like Coordinates of the high symmetry point. label : str Label of the high symmetry point, ready to be plotted. relative : bool, optional Whether to interpret coordinates as relative or absolute. """ if name in self.hs_names: raise ValueError(f"Point '{name}' already defined.") if not relative: coordinates = absolute_to_relative( coordinates, np.array([self.b1, self.b2, self.b3]) ) self.hs_names.append(name) self.hs_coordinates[name] = np.array(coordinates) self.hs_labels[name] = label
[docs] def remove_hs_point(self, name): r""" Remove high symmetry point. Parameters ---------- name : str Name of the high symmetry point. """ if name in self.hs_names: self.hs_names.remove(name) del self.hs_coordinates[name] del self.hs_labels[name]
################################################################################ # Path attributes # ################################################################################ @property def path(self): r""" K points path. Returns ------- path : list of list of str K points path. Each subpath is a list of the high symmetry points. """ return self._path @path.setter def path(self, new_path): if isinstance(new_path, str): tmp_path = new_path.split("|") new_path = [] for i in range(len(tmp_path)): subpath = tmp_path[i].split("-") # Each subpath has to contain at least two points. if len(subpath) != 1: new_path.append(subpath) elif isinstance(new_path, Iterable): tmp_path = new_path new_path = [] for subpath in tmp_path: if isinstance(subpath, str) and "-" in subpath: subpath = subpath.split("-") # Each subpath has to contain at least two points. if len(subpath) != 1: new_path.append(subpath) elif ( not isinstance(subpath, str) and isinstance(subpath, Iterable) and len(subpath) != 1 ): new_path.append(subpath) else: new_path = [tmp_path] break # Check if all points are defined. for subpath in new_path: for point in subpath: if point not in self.hs_names: message = f"Point '{point}' is not defined. Defined points are:" for defined_name in self.hs_names: message += ( f"\n {defined_name} : {self.hs_coordinates[defined_name]}" ) raise ValueError(message) self._path = new_path @property def path_string(self): r""" K points path as a string. Returns ------- path : str """ result = "" for s_i, subpath in enumerate(self.path): for i, name in enumerate(subpath): if i != 0: result += "-" result += name if s_i != len(self.path) - 1: result += "|" return result @property def n(self): r""" Amount of points between each pair of the high symmetry points (high symmetry points excluded). Returns ------- n : int """ return self._n @n.setter def n(self, new_n): if not isinstance(new_n, int): raise ValueError( f"n has to be integer. Given: {new_n}, type = {type(new_n)}" ) self._n = new_n ################################################################################ # Attributes for the axis ticks # ################################################################################ @property def labels(self): r""" Labels of high symmetry points, ready to be plotted. For example for point "Gamma" it returns r"$\Gamma$". If there are two high symmetry points following one another in the path, it returns "X|Y" where X and Y are the labels of the two high symmetry points. Returns ------- labels : list of str Labels, ready to be plotted. Same length as :py:attr:`.ticks`. """ labels = [] for s_i, subpath in enumerate(self.path): if s_i != 0: labels[-1] += "|" + self.hs_labels[subpath[0]] else: labels.append(self.hs_labels[subpath[0]]) for name in subpath[1:]: labels.append(self.hs_labels[name]) return labels
[docs] def coordinates(self, relative=False): raise RuntimeError( "Kpoints.coordinates() was removed in v0.4.0. Use Kpoints.ticks() instead." )
[docs] def ticks(self, relative=False): r""" Tick's positions of the high symmetry points, ready to be plotted. .. versionchanged:: 0.1.2 Renamed from ``coordinates`` Parameters ---------- relative : bool, optional Whether to use relative coordinates instead of the absolute ones. Returns ------- ticks : :numpy:`ndarray` Tick's positions, ready to be plotted. Same length as :py:attr:`.labels`. """ if relative: cell = np.eye(3) else: cell = np.array([self.b1, self.b2, self.b3]) ticks = [] for s_i, subpath in enumerate(self.path): if s_i == 0: ticks.append(0) for i, name in enumerate(subpath[1:]): ticks.append( np.linalg.norm( self.hs_coordinates[name] @ cell - self.hs_coordinates[subpath[i]] @ cell ) + ticks[-1] ) return np.array(ticks)
################################################################################ # Points of the path with intermediate ones # ################################################################################
[docs] def points(self, relative=False): r""" Coordinates of all points with n points between each pair of the high symmetry points (high symmetry points excluded). Parameters ---------- relative : bool, optional Whether to use relative coordinates instead of the absolute ones. Returns ------- points : (N, 3) :numpy:`ndarray` Coordinates of all points. """ if relative: cell = np.eye(3) else: cell = np.array([self.b1, self.b2, self.b3]) points = None for subpath in self.path: for i in range(len(subpath) - 1): name = subpath[i] next_name = subpath[i + 1] new_points = np.linspace( self.hs_coordinates[name] @ cell, self.hs_coordinates[next_name] @ cell, self._n + 2, ) if points is None: points = new_points else: points = np.concatenate((points, new_points)) return points
# It can not just call for points and flatten them, because it has to treat "|" as a special case.
[docs] def flatten_points(self, relative=False): r""" Flatten coordinates of all points with n points between each pair of the high symmetry points (high symmetry points excluded). Used to plot band structure, dispersion, etc. Parameters ---------- relative : bool, optional Whether to use relative coordinates instead of the absolute ones. Returns ------- flatten_points : (N, 3) :numpy:`ndarray` Flatten coordinates of all points. """ if relative: cell = np.eye(3) else: cell = np.array([self.b1, self.b2, self.b3]) flatten_points = None for s_i, subpath in enumerate(self.path): for i in range(len(subpath) - 1): name = subpath[i] next_name = subpath[i + 1] points = ( np.linspace( self.hs_coordinates[name] @ cell, self.hs_coordinates[next_name] @ cell, self._n + 2, ) - self.hs_coordinates[name] @ cell ) delta = np.linalg.norm(points, axis=1) if s_i == 0 and i == 0: flatten_points = delta else: delta += flatten_points[-1] flatten_points = np.concatenate((flatten_points, delta)) return flatten_points
################################################################################ # Copy # ################################################################################
[docs] def copy(self): r""" Create a copy of the kpoints. .. versionadded:: 0.3.0 Returns ------- kpoints : :py:class:`.Kpoints` Copy of the kpoints. """ deepcopy(self)
################################################################################ # Human readables # ################################################################################
[docs] def hs_table(self, decimals=8): r""" High symmetry points table. .. versionadded:: 0.3.1 Parameters ---------- decimals : int, optional Number of decimal places to round the coordinates. Returns ------- table : str String with N+1 lines, where N is the amount of high symmetry points. Each line contains the name of the high symmetry point and its relative and absolute coordinates in a reciprocal space, i.e.:: K1 0.0 0.0 0.0 0.0 0.0 0.0 First line is a header:: Name rel_b1 rel_b2 rel_b3 k_x k_y k_z """ d = decimals table = [ ( f"{'Name':4} " + f"{'rel_b1':>{d+3}} " + f"{'rel_b2':>{d+3}} " + f"{'rel_b3':>{d+3}} " + f"{'k_x':>{d+3}} " + f"{'k_y':>{d+3}} " + f"{'k_z':>{d+3}}" ) ] for name in self.hs_names: relative = self.hs_coordinates[name] i = f"{relative[0]: {d+3}.{d}f}" j = f"{relative[1]: {d+3}.{d}f}" k = f"{relative[2]: {d+3}.{d}f}" absolute = self.hs_coordinates[name] @ np.array([self.b1, self.b2, self.b3]) k_x = f"{absolute[0]: {d+3}.{d}f}" k_y = f"{absolute[1]: {d+3}.{d}f}" k_z = f"{absolute[2]: {d+3}.{d}f}" table.append(f"{name:4} {i} {j} {k} {k_x} {k_y} {k_z}") return "\n".join(table)