Source code for wulfric._kpoints_class

# ================================== LICENSE ===================================
# Wulfric - Cell, Atoms, K-path, visualization.
# 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/>.
#
# ================================ END LICENSE =================================
from copy import deepcopy
from typing import Iterable

import numpy as np

from wulfric.cell._basic_manipulation import get_reciprocal
from wulfric.constants._kpoints import HS_PLOT_NAMES
from wulfric.kpoints._path_and_points import (
    get_path_as_string,
    get_path_as_list,
    get_path_and_points,
)

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


[docs] class Kpoints: r""" Interface for convenient manipulations with the high-symmetry k-points 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 (in the basis of ``rcell``). 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`` and ``names is None, then use "K$_1$", ... "K$_N$", where ``N = len(coordinates)``. If ``None`` but ``names are given, then ``labels = names``. path : str, optional K-path. Use elements of ``names`` to specify the path. If no names given, then use "K1-K2-...-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 in k-path and as main identifier of the point. hs_coordinates : dict Dictionary of the high-symmetry points coordinates. Coordinates are relative (in the basis of ``rcell``) .. code-block:: python {"name": [k_a, k_b, k_c], ... } hs_labels : dict Dictionary of 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 = [] if labels is None: if names is None: labels = [f"K$_{i + 1}$" for i in range(len(coordinates))] else: labels = [name for name in names] elif len(labels) != len(coordinates): raise ValueError( f"Amount of labels ({len(labels)}) does not match amount of points ({len(coordinates)})." ) if names is None: names = [f"K{i + 1}" for i in range(len(coordinates))] elif len(names) != len(coordinates): raise ValueError( f"Amount of names ({len(names)}) does not match amount of points ({len(coordinates)})." ) 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_crystal( cell, atoms, convention="HPKOT", with_time_reversal=True, n=100, spglib_data=None, ): r""" Parameters ---------- cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. atoms : dict Dictionary with N atoms. Expected keys: * "positions" : (N, 3) |array-like|_ Positions of the atoms in the basis of lattice vectors (``cell``). In other words - relative coordinates of atoms. * "names" : (N, ) list of str, optional See Notes * "species" : (N, ) list of str, optional See Notes * "spglib_types" : (N, ) list of int, optional See Notes .. hint:: Pass ``atoms = dict(positions=[[0, 0, 0]], spglib_types=[1])`` if you would like to interpret the ``cell`` alone (effectively assuming that the ``cell`` is a primitive one). convention : str, default "HPKOT" Convention for the definition of the conventional cell. Case-insensitive. Supported: * "HPKOT" for [1]_ * "SC" for [2]_ * "spglib" for |spglib|_ with_time_reversal : bool, default True Whether to assume that the system has time reversal symmetry. By default assumes that the system has it. The strategy for extending the path when ``with_time_reversal=False`` for the crystals without inversion symmetry is described in [1]_. For the systems with inversion symmetry this parameter does nothing. .. versionadded:: 0.6.3 n : int, default 100 Number of intermediate points between each pair of the high-symmetry points (high-symmetry points excluded). spglib_data : :py:class:`.SyntacticSugar`, optional If you need more control on the parameters passed to the spglib, then you can get ``spglib_data`` manually and pass it to this function. Use wulfric's interface to |spglib|_ as .. code-block:: python spglib_data = wulfric.get_spglib_data(...) using the same ``cell`` and ``atoms["positions"]`` that you are passing to this function. Notes ----- |spglib|_ uses ``types`` to distinguish the atoms. To see how wulfric deduces the ``types`` for given atoms see :py:func:`wulfric.get_spglib_types`. If two atoms ``i`` and ``j`` have the same spglib_type (i.e. ``atoms["spglib_types"][i] == atoms["spglib_types"][j]``), but they have different property that is stored in ``atoms[key]`` (i.e ``atoms[key][i] != atoms[key][j]``), then those two atoms are considered equal. In the returned ``conventional_atoms`` the value of the ``conventional_atoms[key]`` are populated based on the *last* found atom in ``atoms`` with each for spglib_type. This rule do not apply to the "positions" key. References ---------- .. [1] Hinuma, Y., Pizzi, G., Kumagai, Y., Oba, F. and Tanaka, I., 2017. Band structure diagram paths based on crystallography. Computational Materials Science, 128, pp.140-184. .. [2] Setyawan, W. and Curtarolo, S., 2010. High-throughput electronic band structure calculations: Challenges and tools. Computational materials science, 49(2), pp. 299-312. """ path, points = get_path_and_points( cell=cell, atoms=atoms, spglib_data=spglib_data, convention=convention, with_time_reversal=with_time_reversal, relative=True, ) names = list(points.keys()) coordinates = list(points.values()) labels = [HS_PLOT_NAMES[name] for name in names] return Kpoints( rcell=get_reciprocal(cell=cell), coordinates=coordinates, names=names, labels=labels, path=path, n=n, )
################################################################################ # High symmetry points # ################################################################################
[docs] def add_hs_point(self, name, coordinate, label=None, 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, optional Label of the high-symmetry point, ready to be plotted. If ``None``, then ``label = name``. relative : bool, optional Whether to interpret coordinates as relative or absolute. """ if label is None: label = name if name in self.hs_names: raise ValueError(f'Point "{name}" already defined.') if not relative: # Transform from Cartesian coordinates to relative coordinates. coordinate = coordinate @ np.linalg.inv(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. .. code-block:: python path = [[K1, ..., KN], ...] """ return self._path @path.setter def path(self, new_path): if isinstance(new_path, str): new_path = get_path_as_list(path_as_string=new_path) elif isinstance(new_path, Iterable): tmp_path = new_path new_path = [] for subpath in tmp_path: if not isinstance(subpath, Iterable) or len(subpath) < 2: raise ValueError( f'Expected at least two points in each subpath, got "{subpath}"' ) subpath = [str(name) for name in subpath] new_path.append(subpath) # Check if all points are defined. for subpath in new_path: for point in subpath: if point not in self.hs_names: raise ValueError( ( f"Point '{point}' is not defined. Defined points are:\n " + "\n ".join( [ f"{name} : {self.hs_coordinates[name]}" for name in self.hs_names ] ) ) ) self._path = new_path @property def path_string(self) -> str: r""" K points path as a string. Returns ------- path : str """ return get_path_as_string(path_as_list=self._path) @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} of 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 : (N, ) 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. Same coordinated as in :py:meth:`.flat_points`. Parameters ---------- relative : bool, default False Whether to use relative coordinates instead of the absolute ones. Returns ------- ticks : (N, ) :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, default False 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 flat_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, default False Whether to use relative coordinates instead of the absolute ones. Returns ------- flat_points : (N, 3) :numpy:`ndarray` Flatten coordinates of all points. """ if relative: cell = np.eye(3) else: cell = self.rcell flat_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: flat_points = delta else: delta += flat_points[-1] flat_points = np.concatenate((flat_points, delta)) return flat_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 nd = max([len(name) for name in self.hs_names]) table = [ ( f"{'Name':<{nd}} " + 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:<{nd}} {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