Source code for wulfric._kpoints_class

# Wulfric - Cell, Atoms, K-path.
# Copyright (C) 2023-2025 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.cell._basic_manipulation import get_reciprocal
from wulfric.cell._kpoints import get_hs_data
from wulfric.cell._lepage import lepage
from wulfric.geometry._geometry import absolute_to_relative

# Save local scope at this moment
old_dir = set(dir())
old_dir.add("old_dir")


[docs] class Kpoints: r""" Interface for convenient manipulation with the high symmetry kpoints and K-path in reciprocal space. Parameters ---------- rcell : (3, 3) |array-like|_ Reciprocal cell. Rows are interpreted as vectors. coordinates : list, optional Coordinates of high symmetry points given in relative coordinates in reciprocal space. names: list, optional Names of the high symmetry points. Used in ``path``. Has to have the same length as ``coordinates``. If ``None``, then use "K1", ... "KN", where ``N = len(coordinates)``. labels : list, optional List of the high symmetry point's labels. Used for plotting. Has to have the same length as ``coordinates``. If ``None``, then use "K$_1$", ... "K$_N$", where ``N = len(coordinates)``. path : str, optional K-path. Use elements of ``names`` to specify the path. If no names given, then use "K1", ... "KN", where ``N = len(coordinates)``. n : int Number of intermediate points between each pair of the high symmetry points (high symmetry points excluded). Attributes ---------- rcell : (3, 3) :numpy:`ndarray` Reciprocal cell. Rows are interpreted as vectors. 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, rcell, coordinates=None, names=None, labels=None, path=None, n=100 ) -> None: self.rcell = np.array(rcell) 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
[docs] @staticmethod def from_cell( cell, lattice_type=None, lattice_variation=None, S_matrix=None, C_matrix=None, length_tolerance=1e-8, angle_tolerance=1e-4, n=100, ): r""" Creates an instance of the :py:class:`wulf.Kpoints` from ``cell``. Parameters ---------- cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. lattice_type : str, optional One of the 14 lattice types that correspond to the provided ``cell``, case-insensitive. If not provided, then computed automatically from ``cell``. If provided, then it user's responsibility to ensure that ``lattice_type`` is correct. lattice_variation : str, optional One of the lattice variations that correspond to the provided ``cell`` and ``lattice_type``. If not provided, then computed automatically. Case-insensitive. S_matrix : (3, 3) |array-like|_, optional Transformation matrix S. If not provided, then computed automatically from ``cell``. If provided, then it is user's responsibility to ensure that the matrix is the correct one for the given ``cell``. C_matrix : (3, 3) |array-like|_, optional Transformation matrix C. If not provided, then computed automatically from ``cell``. If provided, then it is user's responsibility to ensure that the matrix is the correct one for the given ``cell``. length_tolerance : float, default :math:`10^{-8}` Tolerance for length variables (lengths of the lattice vectors). Default value is chosen in the contexts of condense matter physics, assuming that length is given in Angstroms. Please choose appropriate tolerance for your problem. angle_tolerance : float, default :math:`10^{-4}` Tolerance for angle variables (angles of the lattice). Default value is chosen in the contexts of condense matter physics, assuming that angles are in degrees. Please choose appropriate tolerance for your problem. n : int, default 100 Number of points between each pair of the high symmetry points (high symmetry points excluded). Returns ------- kp : :py:class:`wulf.Kpoints` """ coordinates, names, labels, path = get_hs_data( cell, lattice_type=lattice_type, lattice_variation=lattice_variation, S_matrix=S_matrix, C_matrix=C_matrix, length_tolerance=length_tolerance, angle_tolerance=angle_tolerance, ) return Kpoints( get_reciprocal(cell), coordinates=coordinates, names=names, labels=labels, path=path, n=n, )
################################################################################ # High symmetry points # ################################################################################
[docs] def add_hs_point(self, name, coordinate, label, relative=True) -> None: r""" Adds high symmetry point. Parameters ---------- name : str Name of the high symmetry point. coordinate : (3,) array-like Coordinate 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: coordinate = absolute_to_relative(coordinate, self.rcell) self.hs_names.append(name) self.hs_coordinates[name] = np.array(coordinate) self.hs_labels[name] = label
[docs] def remove_hs_point(self, name) -> None: r""" Removes 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) -> list: 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) -> str: 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) -> int: 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) -> list: 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 ticks(self, relative=False): r""" Tick's positions of the high symmetry points, ready to be plotted. 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 = self.rcell 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 = self.rcell 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 = self.rcell 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""" Creates a copy of the kpoints. Returns ------- kpoints : :py:class:`.Kpoints` Copy of the kpoints. """ return deepcopy(self)
################################################################################ # Human readables # ################################################################################
[docs] def hs_table(self, decimals=8) -> str: r""" Table of the high symmetry points. 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] @ self.rcell 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)
# Populate __all__ with objects defined in this file __all__ = list(set(dir()) - old_dir) # Remove all semi-private objects __all__ = [i for i in __all__ if not i.startswith("_")] del old_dir