Basic notation#
On this page we introduce formal definition of the vector and cell and describe how they are stored in wulfric.
Wulfric stores and manipulates both column vectors and row vectors as (3,) NumPy arrays (i. e. the code does not explicitly distinguish the row and column vectors)
import numpy as np
vector_column = np.array([vx, vy, vz])
vector_row = np.array([vx, vy, vz])
When simply the word "vector" is used, we assume a column vector for the mathematical formulas.
import numpy as np
vector = np.array([vx, vy, vz])
One-dimensional NumPy arrays do not distinguish between row and column vectors.
Therefore, if the code example contains a vector r, it may mean either
\(\boldsymbol{r}\) or \(\boldsymbol{r}^T\).
Cell#
Cell is defined by three lattice vectors (or "basis vectors") \(\boldsymbol{a}_i = (a_i^x, a_i^y, a_i^z)^T\). Wulfric stores those vectors in a form of as a \((3\times3)\) matrix
import numpy as np
cell = np.array([
[a1_x, a1_y, a1_z],
[a2_x, a2_y, a2_z],
[a3_x, a3_y, a3_z],
])
where each vector is a row
a1 = cell[0]
a2 = cell[1]
a3 = cell[2]
Note
Wulfric defines cell as a transpose of the standard definition in International Tables for Crystallography or of spglib (However, it is the same as in spglib's python interface).
We deliberately choose to define the cell in that way for consistency between the mathematical formulas and python code. Formally one can substitute \(\boldsymbol{A} = \boldsymbol{A}^T_{\text{ITA}}\) and recover the same formulas as in International Tables for Crystallography for most cases. We try to define the action of the transformation and rotation matrices in the same way as in International Tables for Crystallography.
Position of atom#
Atom's position can be stored in two distinct ways
\(\boldsymbol{r}\) - as absolute position in the global Cartesian reference frame
\(\boldsymbol{x}\) - as relative position with respect to some Cell
In wulfric atom's position is always stored and returned as relative.
import numpy as np
x = np.array([x1, x2, x3])
Cartesian (absolute) position of the atom (also called "radius vector") can be calculated as
r = x @ cell
# or
r = cell.T @ x
Note
Remember that one-dimensional NumPy arrays effectively do not distinguish between row and column vectors in the context of matrix multiplication.
Reciprocal cell#
Reciprocal cell is defined by three reciprocal lattice vectors \(\boldsymbol{b}_i = (b_i^x, b_i^y, b_i^z)^T\). Wulfric stores those vectors in a form of as a \((3\times3)\) matrix
import numpy as np
reciprocal_cell = np.array([
[b1_x, b1_y, b1_z],
[b2_x, b2_y, b2_z],
[b3_x, b3_y, b3_z],
])
Reciprocal cell is connected with the direct cell of the lattice as
import numpy as np
reciprocal_cell = 2 * np.pi * np.linalg.inv(cell.T)
K-points#
Similar to Position of atom, k-point can be stored in two distinct ways
\(\boldsymbol{k}\) - as absolute position in the global Cartesian reference frame
\[\begin{split}\boldsymbol{k} = (k_1,k_2,k_3)^T = \begin{pmatrix} k_1 \\ k_2 \\ k_3 \end{pmatrix}\end{split}\]import numpy as np k = np.array([k1, k2, k3])
\(\boldsymbol{g}\) - as relative position with respect to some Reciprocal cell
\[\begin{split}\boldsymbol{g} = (g_1,g_2,g_3)^T = \begin{pmatrix} g_1 \\ g_2 \\ g_3 \end{pmatrix}\end{split}\]import numpy as np g = np.array([g1, g2, g3])
In wulfric k-point's storage/return mode can be controlled with the keyword argument
relative=True or relative=False in almost all functions and methods. Often
relative=True by default.
With the known cell \(\boldsymbol{B}\) change between relative and absolute k point position is straightforward
Relative -> absolute
\[\begin{split}\boldsymbol{k}^T &= \boldsymbol{g}^T \boldsymbol{B}\\ \boldsymbol{k} &= \boldsymbol{B}^T \boldsymbol{g}\end{split}\]k = g @ reciprocal_cell # or k = reciprocal_cell.T @ g
Cartesian (absolute) -> relative
\[\begin{split}\boldsymbol{g}^T &= \boldsymbol{k}^T \boldsymbol{B}^{-1}\\ \boldsymbol{g} &= \left(\boldsymbol{B}^T\right)^{-1} \boldsymbol{k}\end{split}\]g = k @ np.linalg.inv(reciprocal_cell) # or g = np.linalg.inv(reciprocal_cell.T) @ k
Transformation of the cell#
Choice of the cell is not unique for any given lattice. Transformation from the original cell \(\boldsymbol{A}\) to the transformed cell \(\boldsymbol{\tilde{A}}\) is expressed with the transformation matrix \(\boldsymbol{P}\) as
import numpy as np
# tilde_cell is \tilde{A}
tilde_cell = P.T @ cell
cell = np.linalg.inv(P.T) @ tilde_cell
Note
We deliberately define action of the transformation with the transposition sign. When its action is defined in that way matrix \(\boldsymbol{P}\) is the same as the transformation matrix \(\boldsymbol{P}\) in International Tables for Crystallography Volume A, Chapter 5.1.
It is important to understand that the transformation of the cell describes the choice of the cell for the given lattice or crystal. In other words while the cell is changed, the lattice or crystal remains intact. Consecutively, the Cartesian position of atom is not changed (\(\boldsymbol{r} = \boldsymbol{\tilde{r}}\)), while its relative position is transformed as
import numpy as np
tilde_x = np.linalg.inv(P) @ x
# or
tilde_x = x @ np.linalg.inv(P).T
Reciprocal cell is changed by the transformation as
import numpy as np
# tilde_reciprocal_cell <- \tilde{B}
tilde_reciprocal_cell = np.linalg.inv(P) @ reciprocal_cell
reciprocal_cell = P @ tilde_reciprocal_cell
Cartesian position of k-point does not change, but relative position of k-point is transformed as
import numpy as np
tilde_g = P.T @ g
# or
tilde_g = g @ P
Transformation matrix itself can be computed from original and transformed direct cells
(implemented in wulfric.cell.get_transformation_matrix())
import numpy as np
P = np.linalg.inv(cell).T @ tilde_cell
or from original and transformed reciprocal cells
import numpy as np
tilde_reciprocal_cell = np.linalg.inv(P) @ reciprocal_cell
reciprocal_cell = P @ tilde_reciprocal_cell
Rotation of the cell#
On contrary to the Transformation of the cell, rotation changes the orientation of the lattice or crystal, while keeping the same choice of the cell.
Rotation of the given cell \(\boldsymbol{A}\) from its original orientation to the new orientation of the same cell \(\boldsymbol{A}_{\text{rotated}}\) is expressed with the rotation matrix \(\boldsymbol{R}\) as
import numpy as np
# rotated_cell is A_{rotated}
rotated_cell = cell @ R.T
# Note that inverse of rotation matrix is equivalent to its transpose
cell = rotated_cell @ R
If action of the rotation matrix is defined as above, then it acts on the column vectors in the usual way
v_rotated = R @ v
# or
v_rotated = v @ R.T
Note
Remember that one-dimensional NumPy arrays effectively do not distinguish between row and column vectors in the context of matrix multiplication.
Relative position of atom is not changed by the rotation (\(\boldsymbol{x} = \boldsymbol{\tilde{x}}\)), while its Cartesian position is rotated as
import numpy as np
r_rotated = R @ r
# or
r_rotated = r @ R.T
Reciprocal cell is rotated as
import numpy as np
# reciprocal_cell_rotated is B_{rotated}
reciprocal_cell_rotated = reciprocal_cell @ R.T
# Note that inverse of rotation matrix is equivalent to its transpose
reciprocal_cell = reciprocal_cell_rotated @ R
Relative position of the k-point do not change, but Cartesian position of k-point is rotated as
import numpy as np
k_rotated = R @ k
# or
k_rotated = k @ R.T
Rotation matrix itself can be computed from original and rotated direct cells
import numpy as np
R = np.linalg.inv(cell_rotated) @ cell
or from original and transformed reciprocal cells
import numpy as np
R = np.linalg.inv(reciprocal_cell_rotated) @ reciprocal_cell