Source code for radtools.routines

r"""
Collection of small routines and constants, 
which are used across the whole package.

It's purpose is to serve as an "other" folder.
"""

import sys
from math import asin, cos, pi, sin, sqrt

from matplotlib.colors import LinearSegmentedColormap, to_rgb

from typing import Iterable

import numpy as np
from termcolor import cprint, colored

__all__ = [
    "print_2d_array",
    "volume",
    "angle",
    "absolute_to_relative",
    "get_permutation",
    "cell_from_param",
    "reciprocal_cell",
    "span_orthonormal_set",
]

RED = "#FF4D67"
GREEN = "#58EC2E"
ORANGE = "#F7CB3D"
BLUE = "#274DD1"
PURPLE = "#DC5CFF"

# Constants are usually defined in uppercase
# but these two are intentionally defined in lowercase
todegrees = 180 / pi
toradians = pi / 180


def atom_mark_to_latex(mark):
    r"""
    Latexifier for atom marks.

    Cr12 -> Cr\ :sub:`12`\.

    Parameters
    ----------
    mark : str
        Mark of atom.

    Returns
    -------
    new_mark : str
        Latex version of the mark.
    """
    numbers = "0123456789"
    new_mark = "$"
    insert_underline = False
    for symbol in mark:
        if symbol in numbers and not insert_underline:
            insert_underline = True
            new_mark += "_{"
        new_mark += symbol
    new_mark += "}$"
    return new_mark


def rot_angle(x, y, dummy=False):
    r"""
    Rotational angle from 2D vector.

    Mathematically positive => counterclockwise.
    From [0 to 360)

    Parameters
    ----------
    x : float or int
        x coordinate of a vector.
    y : float or int
        y coordinate of a vector.
    """
    # rot_cos = x / (x ** 2 + y ** 2) ** 0.5
    # rot_angle = m.acos(rot_cos) / m.pi * 180
    try:
        sin = abs(y) / sqrt(x**2 + y**2)
    except ZeroDivisionError:
        raise ValueError("Angle is ill defined (x = y = 0).")
    if x > 0:
        if y > 0:
            return asin(sin) / pi * 180
        elif y == 0:
            return 0
        elif y < 0:
            if not dummy:
                return -asin(sin) / pi * 180
            return 360 - asin(sin) / pi * 180
    elif x == 0:
        if y > 0:
            return 90
        elif y == 0:
            raise ValueError("Angle is ill defined (x = y = 0).")
        elif y < 0:
            if not dummy:
                return 90
            return 270
    elif x < 0:
        if y > 0:
            if not dummy:
                return -asin(sin) / pi * 180
            return 180 - asin(sin) / pi * 180
        elif y == 0:
            if not dummy:
                return 0
            return 180
        elif y < 0:
            if not dummy:
                return asin(sin) / pi * 180
            return 180 + asin(sin) / pi * 180


[docs] def absolute_to_relative(cell, absolute): r""" Compute relative coordinates with respect to the unit cell. Parameters ---------- cell : (3, 3) |array_like|_ Lattice vectors. absolute : (3,) |array_like|_ Absolute coordinates. Returns ------- relative : (3,) :numpy:`ndarray` Relative coordinates. """ # Three vectors of the cell v1 = np.array(cell[0], dtype=float) v2 = np.array(cell[1], dtype=float) v3 = np.array(cell[2], dtype=float) v = np.array(absolute, dtype=float) if (v == np.zeros(3)).all(): return np.zeros(3) # Compose system of linear equations B = np.array([np.dot(v1, v), np.dot(v2, v), np.dot(v3, v)]) A = np.array( [ [np.dot(v1, v1), np.dot(v1, v2), np.dot(v1, v3)], [np.dot(v2, v1), np.dot(v2, v2), np.dot(v2, v3)], [np.dot(v3, v1), np.dot(v3, v2), np.dot(v3, v3)], ] ) # Solve and return return np.linalg.solve(A, B)
[docs] def volume(*args): r""" Computes volume. .. versionadded:: 0.7 Three type of arguments are expected: * One argument. Matrix ``cell``. Volume is computed as: .. math:: V = v_1 \cdot (v_2 \times v_3) * Three arguments. Vectors ``v1``, ``v2``, ``v3``. Volume is computed as: .. math:: V = v_1 \cdot (v_2 \times v_3) * Six arguments. Parameters ``a``, ``b``, ``c``, ``alpha``, ``beta``, ``gamma``. Volume is computed as: .. math:: V = abc\sqrt{1+2\cos(\alpha)\cos(\beta)\cos(\gamma)-\cos^2(\alpha)-\cos^2(\beta)-\cos^2(\gamma)} Parameters ---------- v1 : (3,) |array_like|_ First vector. v2 : (3,) |array_like|_ Second vector. v3 : (3,) |array_like|_ Third vector. cell : (3,3) |array_like|_ Cell matrix, rows are interpreted as vectors. a : float, default 1 Length of the :math:`v_1` vector. b : float, default 1 Length of the :math:`v_2` vector. c : float, default 1 Length of the :math:`v_3` vector. alpha : float, default 90 Angle between vectors :math:`v_2` and :math:`v_3`. In degrees. beta : float, default 90 Angle between vectors :math:`v_1` and :math:`v_3`. In degrees. gamma : float, default 90 Angle between vectors :math:`v_1` and :math:`v_2`. In degrees. Returns ------- volume : float Volume of corresponding region in space. """ if len(args) == 1: v1, v2, v3 = np.array(args[0]) elif len(args) == 3: v1, v2, v3 = np.array(args) elif len(args) == 6: a, b, c, alpha, beta, gamma = args alpha = alpha * toradians beta = beta * toradians gamma = gamma * toradians return ( a * b * c * sqrt( 1 + 2 * cos(alpha) * cos(beta) * cos(gamma) - cos(alpha) ** 2 - cos(beta) ** 2 - cos(gamma) ** 2 ) ) else: raise ValueError( "Unable to identify input. " + "Supported: one (3,3) array_like, or three (3,) array_like, or 6 floats." ) return v1 @ np.cross(v2, v3)
def winwait(): r""" Add "Press Enter to continue" behaviour to Windows. Its a hotfix for Window`s terminal behaviour. """ if sys.platform == "win32": cprint("Press Enter to continue", "green") input()
[docs] def angle(v1, v2, radians=False): r""" Angle between two vectors. .. versionadded:: 0.7 .. math:: \alpha = \dfrac{(\vec{v_1} \cdot \vec{v_2})}{\vert\vec{v_1}\vert\cdot\vert\vec{v_2}\vert} Parameters ---------- v1 : (3,) |array_like|_ First vector. v2 : (3,) |array_like|_ Second vector. radians : bool, default False Whether to return value in radians. Return value in degrees by default. Returns ------- angle: float Angle in degrees or radians (see ``radians``). """ # Normalize vectors v1 = np.array(v1) / np.linalg.norm(v1) v2 = np.array(v2) / np.linalg.norm(v2) alpha = np.arccos(np.clip(np.dot(v1, v2), -1.0, 1.0)) if radians: return alpha return alpha * todegrees
[docs] def reciprocal_cell(cell): r""" Computes reciprocal cell. .. versionadded:: 0.7 Parameters ---------- cell : (3, 3) |array_like|_ Cell matrix, rows are interpreted as vectors. Returns ------- reciprocal_cell : (3, 3) :numpy:`ndarray` Reciprocal cell matrix, rows are interpreted as vectors. :math:`cell = (\vec{v}_1, \vec{v}_2, \vec{v}_3)`, where .. math:: \begin{matrix} \vec{b}_1 = \dfrac{2\pi}{V}\vec{a}_2\times\vec{a}_3 \\ \vec{b}_2 = \dfrac{2\pi}{V}\vec{a}_3\times\vec{a}_1 \\ \vec{b}_3 = \dfrac{2\pi}{V}\vec{a}_1\times\vec{a}_2 \\ \end{matrix} """ vol = volume(cell) reciprocal_cell = np.array( [ 2 * pi / vol * np.cross(cell[1], cell[2]), 2 * pi / vol * np.cross(cell[2], cell[0]), 2 * pi / vol * np.cross(cell[0], cell[1]), ] ) return reciprocal_cell
[docs] def cell_from_param(a=1, b=1, c=1, alpha=90, beta=90, gamma=90): r""" Return cell from lattice parameters. .. versionadded:: 0.7 Parameters ---------- a : float, default 1 Length of the :math:`a_1` vector. b : float, default 1 Length of the :math:`a_2` vector. c : float, default 1 Length of the :math:`a_3` vector. alpha : float, default 90 Angle between vectors :math:`a_2` and :math:`a_3`. In degrees. beta : float, default 90 Angle between vectors :math:`a_1` and :math:`a_3`. In degrees. gamma : float, default 90 Angle between vectors :math:`a_1` and :math:`a_2`. In degrees. Returns ------- cell : (3, 3) :numpy:`ndarray` Cell matrix. .. code-block:: python cell = [[a1_x, a1_y, a1_z], [a2_x, a2_y, a2_z], [a3_x, a3_y, a3_z]] """ alpha = alpha * toradians beta = beta * toradians gamma = gamma * toradians return np.array( [ [a, 0, 0], [b * cos(gamma), b * sin(gamma), 0], [ c * cos(beta), c / sin(gamma) * (cos(alpha) - cos(beta) * cos(gamma)), c / sin(gamma) * sqrt( 1 + 2 * cos(alpha) * cos(beta) * cos(gamma) - cos(alpha) ** 2 - cos(beta) ** 2 - cos(gamma) ** 2 ), ], ] )
def param_from_cell(cell): r""" Return lattice parameters from cell. .. versionadded:: 0.7 Parameters ---------- cell : (3,3) |array_like|_ Cell matrix, rows are interpreted as vectors. .. code-block:: python cell = [[a1_x, a1_y, a1_z], [a2_x, a2_y, a2_z], [a3_x, a3_y, a3_z]] Returns ------- a : float Length of the :math:`a_1` vector. b : float Length of the :math:`a_2` vector. c : float Length of the :math:`a_3` vector. alpha : float Angle between vectors :math:`a_2` and :math:`a_3`. In degrees. beta : float Angle between vectors :math:`a_1` and :math:`a_3`. In degrees. gamma : float Angle between vectors :math:`a_1` and :math:`a_2`. In degrees. """ return ( np.linalg.norm(cell[0]), np.linalg.norm(cell[1]), np.linalg.norm(cell[2]), angle(cell[1], cell[2]), angle(cell[0], cell[2]), angle(cell[0], cell[1]), )
[docs] def get_permutation(n, k): r""" Return array of index permutations .. versionadded:: 0.7 Parameters ---------- n : int Length of the array to be permuted. .. code-block:: range(0, n) k : int Length of the permutation. Returns ------- permutations : list List of permutations. If N permutations are found, the it is a list of N lists of length k. """ if k == n: return [[i for i in range(n)]] elif n < k: raise ValueError("Permutations: n < k") else: result = [[i] for i in range(n)] true_k = k k = 0 while k < true_k - 1: new_result = [] for i in range(len(result)): for j in range(result[i][-1] + 1, n): new_result.append(result[i] + [j]) result = new_result k += 1 return result
[docs] def span_orthonormal_set(vec): r""" Span orthonormal set of vectors. Parameters ---------- vec : (3,) |array_like|_ Vector, which serves as :math:`e_3` Returns ------- e1 : (3,) :numpy:`ndarray` e2 : (3,) :numpy:`ndarray` e3 : (3,) :numpy:`ndarray` """ e3 = np.array(vec) / np.linalg.norm(vec) x, y, z = e3 if x != 0: ny = 0 nz = x / sqrt(x**2 + z**2) nx = -nz * z / x elif y != 0: nz = 0 nx = y / sqrt(x**2 + y**2) ny = -nx * x / y else: nx = 0 ny = z / sqrt(y**2 + z**2) nz = -ny * y / z e2 = np.array([nx, ny, nz]) e1 = np.cross(e2, e3) return e1, e2, e3
if __name__ == "__main__": print_2d_array([[1, 2], [3, 4], [5, 6]]) print_2d_array([[1, 2], [3, 4], [5, 6]], fmt="10.2f") print_2d_array([[1, 2], [3, 4], [5, 6]], fmt=".2f") print_2d_array([[1, 2], [3, 4], [52414345345, 6]], fmt="10.2E") print_2d_array([[1, 2 + 1j], [3, 4], [52, 6]]) print_2d_array([[1, 2 - 1j], [3, 4], [52, 6]]) print_2d_array([[1, -1j], [3, 4], [52, 6]]) print_2d_array([]) print_2d_array([[1, 2], [3, 4], [5, 6]], highlight=True) print_2d_array([[1, 2], [3, 4], [5, 6]], fmt="10.2f", highlight=True) print_2d_array([[1, 2], [3, 4], [5, 6]], fmt=".2f", highlight=True) print_2d_array([[1, 2], [3, 4], [52414345345, 6]], fmt="10.2E", highlight=True) print_2d_array([[1, 2 + 1j], [3, 4], [52, 6]], highlight=True) print_2d_array([[1, 2 - 1j], [3, 4], [52, 6]], highlight=True) print_2d_array([[1, -1j], [3, 4], [52, 6]], highlight=True) print_2d_array([[1, 2], [3, 4], [5, 6]], highlight=True, borders=False) print_2d_array([[1, 2], [3, 4], [5, 6]], fmt="10.2f", highlight=True, borders=False) print_2d_array([[1, 2], [3, 4], [5, 6]], fmt=".2f", highlight=True, borders=False) print_2d_array( [[1, 2], [3, 4], [52414345345, 6]], fmt="10.2E", highlight=True, borders=False ) print_2d_array([[1, 2 + 1j], [3, 4], [52, 6]], highlight=True, borders=False) print_2d_array([[1, 2 - 1j], [3, 4], [52, 6]], highlight=True, borders=False) print_2d_array([[1, -1j], [3, 4], [52, 6]], highlight=True, borders=False) print_2d_array([[1, 2], [3, 4], [5, 6]], highlight=True, borders=False, shift=8) print_2d_array([[1, 2], [3, 4], [5, 6]], highlight=True, shift=8) def plot_horizontal_lines(ax, positions): r""" Plot horizontal lines. Parameters ---------- ax : :matplotlib:`axes` Axes to plot on. positions : list List of y positions. """ if not isinstance(positions, Iterable): positions = [positions] ax.hlines( positions, 0, 1, transform=ax.get_yaxis_transform(), color="grey", linewidths=0.5, linestyles="dashed", ) def plot_vertical_lines(ax, positions): r""" Plot vertical lines. Parameters ---------- ax : :matplotlib:`axes` Axes to plot on. positions : list List of x positions. """ if not isinstance(positions, Iterable): positions = [positions] ax.vlines( positions, 0, 1, transform=ax.get_xaxis_transform(), color="grey", linewidths=0.5, linestyles="dashed", ) def custom_cmap(start_color, finish_color): r""" Prepare custom colormap. From one color to the other. Parameters ---------- start_color : Color Start color. finish_color : Color Finish color. """ r1, g1, b1 = to_rgb(start_color) r2, g2, b2 = to_rgb(finish_color) cdict = { "red": ((0, r1, r1), (1, r2, r2)), "green": ((0, g1, g1), (1, g2, g2)), "blue": ((0, b1, b1), (1, b2, b2)), } return LinearSegmentedColormap("custom_cmap", cdict, N=256)