Transformations

A transformation basically corresponds to a transformation of lattice vectors. But when these vectors change (in other words “are re-defined in terms of the initial lattice vectors”), along with them, the site coordinates, the lattice parameters (which are defined through the lattice vectors), the metric tensor and the representations of the symmetry operators also change.

Change of basis

Imagine a 2D grid of square cells with edge length being \(l\).

Transformations_SquareGrid.png

We can refer the sites in the unit cell(s) using the blue basis \((\vec a,\vec b)\), where, a site at the center would have the coordinates of \((\tfrac{1}{2},\tfrac{1}{2})\). Or, we can instead use the purple basis \((\vec a',\vec b')\) and the \((\tfrac{1}{2},\tfrac{1}{2})\) site in the blue basis would now be located at \((\tfrac{1}{2},0)\) in this basis. Similarly, \((0,1)\) would be \((\tfrac{1}{2},\tfrac{1}{2})\) in this basis set.

In order to systematically analyze the effects of such a transformation, we should first write the new basis set in terms of the old one. For this simple square grid case, we immediately see (from vector addition rules) that:

\[\begin{split}\begin{align}\vec a' &= \vec a + \vec b\\ \vec b' &= -\vec a + \vec b \end{align}\end{split}\]

Writing the two equations above as a matrix production, we have:

\[\begin{split}(\vec a',\vec b') = (\vec a,\vec b)\begin{pmatrix}1 &-1\\1&1\end{pmatrix}\end{split}\]

By writing the transformation of the basis in this form, we have thus identified the essential transformation matrix \(P\) which is sufficient by itself for the description of all the properties of the new system from the old one’s. So we start with the basis transformation under a transformation matrix \(P\):

\[(\vec a',\vec b') = (\vec a,\vec b)P\]

Going back

If a transformation \(P\) yields \((\vec a',\vec b')\) from \((\vec a,\vec b)\), the inverse is also valid: \((\vec a,\vec b)\) is connected to \((\vec a',\vec b')\) via \(P^{-1}\) in the same way:

\[(\vec a,\vec b) = (\vec a',\vec b')P^{-1}\]

abc matrix notation

For the transformation matrices, the shorthand notation uses the concept of “abc” where by reading the matrix column-wise, actually each new basis’ correspondence is included. For example, the above transformation matrix:

\[\begin{split}\begin{pmatrix}1 &-1\\1&1\end{pmatrix}\end{split}\]

is read as: \(a+b,-a+b\).

For another example, a transformation matrix such as:

\[\begin{split}\begin{pmatrix} 1&0&1\\-1&1&1\\0&-1&1\end{pmatrix}\end{split}\]

(which we will be using below) is read as: \(a-b,b-c,a+b+c\).

Warning

Even though both the symmetry operators and the transformations are represented as \(3\times 3\) matrices, the “xyz” notation is exclusive to the symmetry operators, whereas the “abc” notation is used only for transformations!

Position Transformations

A position \(X\) defined in terms of the old basis set is transformed accordingly with the transformation matrix \(P\) as:

\[X' = P^{-1}X\]

In our square grid example, let’s transform the two mentioned sites of \((\tfrac{1}{2},\tfrac{1}{2})\) and \((0,1)\):

import numpy as np

P = np.array([[1,-1],[1,1]])
P_inv = np.linalg.inv(P)

site_A = np.array([[0.5],[0.5]])
site_B = np.array([[0.],[1.]])

site_A_prime = np.dot(P_inv,site_A)
site_B_prime = np.dot(P_inv,site_B)

print("A: ",site_A.T," -> ",site_A_prime.T)
print("B: ",site_B.T," -> ",site_B_prime.T)
A:  [[0.5 0.5]]  ->  [[0.5 0. ]]
B:  [[0. 1.]]  ->  [[0.5 0.5]]

Operator Transformations

The symmetry operators are transformed via the following rule:

\[W' = P^{-1}WP\]

and under this transformation, all the relations between the symmetry operators of the group are preserved as well, as it should be: if \(AB=C\), then under the transformation, \(A'B'=C'\)

Proof

\[AB=C\]
\[\begin{split}\begin{align*}A'&=P^{-1}AP\\B'&=P^{-1}BP\\C'&=P^{-1}CP\\\end{align*}\end{split}\]
\[\begin{split}\begin{align*}A'B'&=P^{-1}A\underbrace{PP^{-1}}_{1}BP\\ &=P^{-1}\underbrace{AB}_CP\\ &=P^{-1}CP=C' \end{align*}\end{split}\]

Example: Relations preserved under transformation (Rotation along one axis)

Check that the relations between the operators of P4 (#75) space group holds under a transformation of 30° rotation of the basis vectors.

We begin by defining the matrix represenatations of the symmetry operators \(\{1,2,4^+,4^-\}\):

import numpy as np

o1 = np.eye(3,3)
o2 = np.diag([-1,-1,1])
o4p = np.array([[0,-1,0],[1,0,0],[0,0,1]])
o4m = np.array([[0,1,0],[-1,0,0],[0,0,1]])

for op in [o1,o2,o4p,o4m]:
    print(op,"\n","-"*30)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]] 
 ------------------------------
[[-1  0  0]
 [ 0 -1  0]
 [ 0  0  1]] 
 ------------------------------
[[ 0 -1  0]
 [ 1  0  0]
 [ 0  0  1]] 
 ------------------------------
[[ 0  1  0]
 [-1  0  0]
 [ 0  0  1]] 
 ------------------------------

(You can refer to Symmetry Operators for the details of the above definitions)

Defining the transformation matrix and transforming the symmetry operators via it:

# Transformation Matrix
theta = np.deg2rad(30)

P = np.array([[np.cos(theta),-np.sin(theta),0],\
              [np.sin(theta), np.cos(theta),0],\
              [0,0,1]])
P_inv = np.linalg.inv(P)
o1_prime = np.linalg.multi_dot([P_inv,o1,P])
o2_prime = np.linalg.multi_dot([P_inv,o2,P])
o4p_prime = np.linalg.multi_dot([P_inv,o4p,P])
o4m_prime = np.linalg.multi_dot([P_inv,o4m,P])

Checking to see if the following relations hold:

  • \(4'^+4'^-\stackrel{?}{=} 1'\)

print(np.alltrue(np.isclose(np.dot(o4p_prime,o4m_prime), o1_prime)))
True
  • \(4'^+2'\stackrel{?}{=} 4'^-\)

print(np.alltrue(np.isclose(np.dot(o4p_prime,o2_prime), o4m_prime)))
True
  • \(4'^+4'^+\stackrel{?}{=} 2'\)

print(np.alltrue(np.isclose(np.dot(o4p_prime,o4p_prime), o2_prime)))
True
  • \(4'^-4'^-\stackrel{?}{=} 2'\)

print(np.alltrue(np.isclose(np.dot(o4m_prime,o4m_prime), o2_prime)))
True
  • \(\left(4'^+\right)^3\stackrel{?}{=} 4'^-\)

print(np.alltrue(np.isclose(\
                   np.linalg.matrix_power(o4p_prime,3), o4m_prime)))
True

Example: Relations preserved under transformation (Rotation along multiple axes)

In the above example, as we were dealing with rotation operators’ transformation under rotation along only 1 direction, no change in the forms occured:

P4_ops = [o1,o2,o4p,o4m]
P4_ops_prime = [o1_prime,o2_prime,o4p_prime,o4m_prime]

for i in range(len(P4_ops)):
    print (P4_ops[i])
    
    # Removing the clutter...
    P4_ops_prime[i][np.abs(P4_ops_prime[i])<1E-15] = 0
    
    print (P4_ops_prime[i])
    print(np.allclose(P4_ops[i],P4_ops_prime[i]))
    print ("-"*15)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
True
---------------
[[-1  0  0]
 [ 0 -1  0]
 [ 0  0  1]]
[[-1.  0.  0.]
 [ 0. -1.  0.]
 [ 0.  0.  1.]]
True
---------------
[[ 0 -1  0]
 [ 1  0  0]
 [ 0  0  1]]
[[ 0. -1.  0.]
 [ 1.  0.  0.]
 [ 0.  0.  1.]]
True
---------------
[[ 0  1  0]
 [-1  0  0]
 [ 0  0  1]]
[[ 0.  1.  0.]
 [-1.  0.  0.]
 [ 0.  0.  1.]]
True
---------------

This is due to the fact that, rotation operators along a certain axis, commute with each other. As both the basis transformation and the symmetry operators are rotations, the symmetry operators stay the same under this basis transformation, i.e.,

\[W'=P^{-1}WP=WP^{-1}P=W\]

Since they are commuting, we can change the order of the operators: \(P^{-1}W = WP^{-1}\)

So actually, we didn’t achieve much in the previous transformation example.

This time, consider the following basis transformation:

\[\begin{split}P=\begin{pmatrix} 1&0&1\\-1&1&1\\0&-1&1\end{pmatrix}\end{split}\]

This is actually a transformation that connects the Pm3m (#221) space group to R3m (#166) space group (with index 4). Looking at the transformation matrix, we can see that:

\[\begin{split}\begin{align}\vec a' &= \vec a - \vec b \\ \vec b' &= \vec b - \vec c \\ \vec c' &= \vec a + \vec b+\vec c\end{align}\end{split}\]

And here is the comparison of the basis vectors (#221 & #166) in relation with the unit cells of BaTiO3, from two different views (obtained via VESTA):

BaTiO3_cell_221to166_c.png BaTiO3_cell_221to166.png

(The two unit cells have been oriented to offer a better view for comparison)

The CIF files for the two structures can be downloaded from the following links:

and here they are presented in BCS format:

221
5.0 5.0 5.0 90 90 90
3
Ba	1	1a	0.000000	0.000000	0.000000
Ti	2	1b	0.500000	0.500000	0.500000
O	3	3c	0.500000	0.000000	0.500000
166
7.071068 7.071068 8.660254 90.000000 90.000000 120.000000 
3
Ba	1	3a	0.000000	0.000000	0.000000
Ti	2	3b	0.000000	0.000000	0.500000
O	3	9e	0.166667	0.833333	0.333333

As can be checked through the list of the symmetry operators of the Pm3m (#221) space group in the Structure chapter, this group also contains the \(\{1,2,4^+,4^-\}\) operators, in addition to many others (since it contains all the operators of the P4 (#75) space group, this makes P4, a subgroup of Pm3m, and Pm3m a supergroup of P4).

Applying this transformation on the 4 operators yields:

P = np.array([[1,0,1],\
              [-1,1,1],\
              [0,-1,1]])
P_inv = np.linalg.inv(P)

o1_prime2 = np.linalg.multi_dot([P_inv,o1,P])
o2_prime2 = np.linalg.multi_dot([P_inv,o2,P])
o4p_prime2 = np.linalg.multi_dot([P_inv,o4p,P])
o4m_prime2 = np.linalg.multi_dot([P_inv,o4m,P])
ops = [o1,o2,o4p,o4m]
ops_prime2 = [o1_prime2,o2_prime2,o4p_prime2,o4m_prime2]

for i in range(len(ops)):
    print (ops[i])
    
    # Removing the clutter...
    ops_prime2[i][np.abs(ops_prime2[i])<1E-15] = 0
    
    print (ops_prime2[i])
    print(np.allclose(ops[i],ops_prime2[i]))
    print ("-"*15)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
True
---------------
[[-1  0  0]
 [ 0 -1  0]
 [ 0  0  1]]
[[-1.          0.66666667 -0.66666667]
 [ 0.          0.33333333 -1.33333333]
 [ 0.         -0.66666667 -0.33333333]]
False
---------------
[[ 0 -1  0]
 [ 1  0  0]
 [ 0  0  1]]
[[ 0.33333333 -0.33333333 -1.33333333]
 [ 0.66666667  0.33333333 -0.66666667]
 [ 0.66666667 -0.66666667  0.33333333]]
False
---------------
[[ 0  1  0]
 [-1  0  0]
 [ 0  0  1]]
[[-0.33333333  1.          0.66666667]
 [-0.66666667  1.         -0.66666667]
 [-0.66666667  0.          0.33333333]]
False
---------------

So except for the \(1\) operator, all of them has changed (\(1\)’s matrix representation is always the identity matrix, why? \(1'=P^{-1}1P=1P^{-1}P=1\) as identity commutes with every operator!). Let’s check if the relations hold for these transformed operators:

print(np.alltrue(np.isclose(np.dot(o4p_prime2,o4m_prime2), o1_prime2)))
print(np.alltrue(np.isclose(np.dot(o4p_prime2,o2_prime2), o4m_prime2)))
print(np.alltrue(np.isclose(np.dot(o4p_prime2,o4p_prime2), o2_prime2)))
print(np.alltrue(np.isclose(np.dot(o4m_prime2,o4m_prime2), o2_prime2)))
print(np.alltrue(np.isclose(\
                   np.linalg.matrix_power(o4p_prime2,3), o4m_prime2)))
True
True
True
True
True

Metric Transformation

The metric is transformed under \(P\) as:

\[G'=P^T G P\]

Let’s apply this to our case of Pm3m (#221) to R3m (#166) setting transformation via \(P=a-b,b-c,a+b+c\).

The parameters of the structure in Pm3m (#221) setting are:

5.0 5.0 5.0 90 90 90

Using the functions metric_from_vector and parameters_from_metric we had written in the Wyckoff Positions’s Metric Tensor chapter, we can go back and forth between the metric tensor and the lattice parameters:

def metric_from_vector(vec):
    # Constructs the metric tensor from the input lattice vectors
    # Vectors are input as a 3x3, with each lattice vector given
    #   as a row-vector
    # Returns the metric tensor as a 3x3 matrix
      
    G = np.empty((3,3))
    for i in range(3):
        for j in range(i,3):
            G[i,j] = G[j,i] = np.dot(vec[i,:],vec[j,:])
            
    return G

def parameters_from_metric(G):
    a = np.sqrt(G[0,0])
    b = np.sqrt(G[1,1])
    c = np.sqrt(G[2,2])
    
    alpha = np.rad2deg(np.arccos(G[1,2]/(b*c)))
    beta = np.rad2deg(np.arccos(G[0,2]/(a*c)))
    gamma = np.rad2deg(np.arccos(G[0,1]/(a*b)))
    
    return np.array([a,b,c,alpha,beta,gamma])

def par2vec(par):
    # Converts from parametric form to vectorial form
    # Lattice parameters are input as a 6 dimensional vector
    # Returns the lattice vectors as a 3x3 matrix, with each
    #   vector as a row-vector
    a = par[0]
    b = par[1]
    c = par[2]
    alpha = np.deg2rad(par[3])
    beta  = np.deg2rad(par[4])
    gamma = np.deg2rad(par[5])
    
    a1 = a
    vec_a = np.array([a1,0,0])
    
    b1 = b*np.cos(gamma)
    b2 = np.sqrt(b**2 - b1**2)
    vec_b = np.array([b1,b2,0])
    
    c1 = c*np.cos(beta)
    c2 = ((b*c*np.cos(alpha))- b1*c1)/b2
    c3 = np.sqrt(c**2 - c1**2 - c2**2)
    vec_c = np.array([c1,c2,c3])
    
    abc_vec = np.array([vec_a,vec_b,vec_c])
    return abc_vec
    
par_221 = np.array([5.,5,5,90,90,90])
vec_221 = par2vec(par_221)
G_221 = metric_from_vector(vec_221)
print(G_221)
[[2.5000000e+01 1.5308085e-15 1.5308085e-15]
 [1.5308085e-15 2.5000000e+01 1.5308085e-15]
 [1.5308085e-15 1.5308085e-15 2.5000000e+01]]

Transforming the metric:

P = np.array([[1,0,1],\
              [-1,1,1],\
              [0,-1,1]])

G_166 = np.linalg.multi_dot((P.T,G_221,P))
print(G_166)
[[ 5.00000000e+01 -2.50000000e+01  0.00000000e+00]
 [-2.50000000e+01  5.00000000e+01 -3.55271368e-15]
 [ 0.00000000e+00  0.00000000e+00  7.50000000e+01]]
par_166 = parameters_from_metric(G_166)
print(("{:.4f} "*3+"{:.2f} "*3).format(*par_166))
7.0711 7.0711 8.6603 90.00 90.00 120.00 

As mentioned previously, the square root of the determinant of the metric tensor gives the volume of the unit cell:

print("The volume of the unit cell in #221 setting: {:.2f} ų"\
      .format(np.sqrt(np.linalg.det(G_221))))
print("The volume of the unit cell in #166 setting: {:.2f} ų"\
      .format(np.sqrt(np.linalg.det(G_166))))
The volume of the unit cell in #221 setting: 125.00 ų
The volume of the unit cell in #166 setting: 375.00 ų

These calculations can also be conducted via the CELLTRAN tool of the BCS.

BCS_CELLTRAN.png

The output of the above query will be:

BCS_CELLTRAN_results.png