xyz座標の結晶構造をcif形式に変換する

本稿は計算化学等で周期境界条件を用いた計算を行っている方向けのメモです。
並進ベクトル（Translation Vectors: TV）を含む3次元座標からcif形式のファイルを生成する方法を述べます。

* * *

以下のような直交座標系（Cartesian coordinate system）で記述された構造をcif形式に変換するには分率座標系（fractional coordinate system）に変換する必要があります。
```
Si    1.367182000000      4.101546000000      1.367182000000
Si    0.000000000000      0.000000000000      2.734364000000
Si    1.367182000000      1.367182000000      4.101546000000
Si    0.000000000000      2.734364000000      0.000000000000
Si    4.101546000000      4.101546000000      4.101546000000
Si    2.734364000000      0.000000000000      0.000000000000
Si    4.101546000000      1.367182000000      1.367182000000
Si    2.734364000000      2.734364000000      2.734364000000
TV    5.468730000000      0.000000000000      0.000000000000
TV    0.000000000000      5.468730000000      0.000000000000
TV    0.000000000000      0.000000000000      5.468730000000
```
直交座標系の点$(x,y,z)$を分率座標系の点$(x_{\text{frac}},y_{\text{frac}},z_{\text{frac}})$に変換する公式が知られており、
```math
\begin{equation}
v=\sqrt{1-\cos ^2 \alpha-\cos ^2 \beta-\cos ^2 \gamma+2 \cos \alpha \cos \beta \cos \gamma} \quad
\end{equation}
```
として
```math
\left[{\begin{array}{l}
x_{\text{frac}} \\
y_{\text{frac}} \\
z_{\text{frac}}
\end{array}}\right]=\left[\begin{array}{ccc}
1 / a & -\cos \gamma / a \sin \gamma & (\cos \alpha \cos \gamma-\cos \beta) / a v \sin \gamma \\
0 & 1 / b \sin \gamma & (\cos \beta \cos \gamma-\cos \alpha) / b v \sin \gamma \\
0 & 0 & \sin \gamma / c v
\end{array}\right]\left[\begin{array}{l}
x \\
y \\
z
\end{array}\right]
```
とすればよいことが知られています。ここで、$a$、$b$、$c$は各並進ベクトルの長さ、$\alpha$、$\beta$、$\gamma$は各並進ベクトルの夾角です。

これにしたがってxyz形式からcif形式に変換するコードの例を示します。

```py:xyz2cif.py
import math
import os

print("Convert xyz cordinates into cif format. (by xyz2cif.py v1.0)")
filepath = 'C:/Users/'+os.getlogin()+'/Downloads/'
filename_list = ["Si.xyz", "CuO.xyz", "ZnO.xyz"]

for i in range(len(filename_list)):
    target_file = filepath + filename_list[i]
    with open(target_file, encoding='utf-8') as f:
        tmp_atomnum = 0;  tmp_nodenum = 0;  atomcount = 0
        atomnamelist=[]
        coordlist = []
        TVlist = []
        TVcount = 0
        while line := f.readline():
            elem = line.split()
            if len(elem) >= 4:
                if elem[0] == "TV":
                    TVlist.append([float(elem[1]), float(elem[2]), float(elem[3])])
                    TVcount += 1
                    if TVcount == 4:
                        print("Warning: only three TVs are allowed.")
                else:
                    atomnamelist.append(str(elem[0]))
                    coordlist.append([float(elem[1]), float(elem[2]), float(elem[3])])
            if not line:
                break

    # angström
    a_length = TVlist[0][0]
    b_length = math.sqrt(TVlist[1][0]**2.0 + TVlist[1][1]**2.0)
    c_length = math.sqrt(TVlist[2][0]**2.0 + TVlist[2][1]**2.0 + TVlist[2][2]**2.0)
    # radian
    alpha = math.acos(sum([i*j for (i, j) in zip(TVlist[1], TVlist[2])])/(b_length * c_length)) # b-c
    beta  = math.acos(sum([i*j for (i, j) in zip(TVlist[2], TVlist[0])])/(c_length * a_length)) # c-a
    gamma = math.acos(sum([i*j for (i, j) in zip(TVlist[0], TVlist[1])])/(a_length * b_length)) # a-b
    # degree
    degree_alpha = math.acos(sum([i*j for (i, j) in zip(TVlist[1], TVlist[2])])/(b_length * c_length)) * (180/math.pi) # b-c
    degree_beta  = math.acos(sum([i*j for (i, j) in zip(TVlist[2], TVlist[0])])/(c_length * a_length)) * (180/math.pi) # c-a
    degree_gamma = math.acos(sum([i*j for (i, j) in zip(TVlist[0], TVlist[1])])/(a_length * b_length)) * (180/math.pi) # a-b

    print("\n\nTV coordinates to cell parameters < " + filename_list[i])   # TVs as a lower triangular matrix
    print('{:>11.8f}'.format(a_length), '{:>11.8f}'.format(b_length), '{:>11.8f}'.format(c_length), sep=", ")
    print('{:>11.8f}'.format(degree_alpha), '{:>11.8f}'.format(degree_beta), '{:>11.8f}'.format(degree_gamma), sep=", ")
    print("Surface area (ang^2) =", '{:>14.10f}'.format(TVlist[0][0] * TVlist[1][1]))
    print("Cell volume  (ang^3) =", '{:>14.10f}'.format(TVlist[0][0] * TVlist[1][1] * TVlist[2][2]))

    # create a cif file
    outputall_file = filepath + filename_list[i] + ".cif"
    with open(outputall_file, mode='w', encoding='utf-8') as outf:
        outf.write("# generated using xyz2cif.py (by HN)\n" + filename_list[i] + "\n")
        outf.write("_symmetry_space_group_name_H-M   \'P 1\'\n_chemical_name_common ?\n_chemical_formula_sum ?\n")
        outf.write("_cell_length_a   "+'{:>11.8f}'.format(a_length) + "\n")
        outf.write("_cell_length_b   "+'{:>11.8f}'.format(b_length) + "\n")
        outf.write("_cell_length_c   "+'{:>11.8f}'.format(c_length) + "\n")
        outf.write("_cell_angle_alpha   "+'{:>11.8f}'.format(degree_alpha) + "\n")
        outf.write("_cell_angle_beta    "+'{:>11.8f}'.format(degree_beta) + "\n")
        outf.write("_cell_angle_gamma   "+'{:>11.8f}'.format(degree_gamma) + "\n")
        outf.write("_symmetry_Int_Tables_number   1\n_cell_volume   " + '{:>11.8f}'.format(TVlist[0][0] * TVlist[1][1] * TVlist[2][2]) + "\n_cell_formula_units_Z   " + str(len(coordlist)) + "\n")
        outf.write("loop_\n _symmetry_equiv_pos_site_id\n _symmetry_equiv_pos_as_xyz\n  1  'x, y, z'\n")
        outf.write("loop_\n _atom_site_type_symbol\n _atom_site_label\n _atom_site_symmetry_multiplicity\n _atom_site_fract_x\n _atom_site_fract_y\n _atom_site_fract_z\n _atom_site_occupancy\n")
        for j in range(len(atomnamelist)):
            v = math.sqrt(1 - math.cos(alpha)**2 - math.cos(beta)**2 - math.cos(gamma)**2 + 2*math.cos(alpha)*math.cos(beta)*math.cos(gamma))
            a_frac = coordlist[j][0] - coordlist[j][1] / math.tan(gamma) + coordlist[j][2] * ((math.cos(alpha)*math.cos(gamma)-math.cos(beta))/(v*math.sin(gamma)))
            b_frac = coordlist[j][1] / math.sin(gamma) + coordlist[j][2] * ((math.cos(beta)*math.cos(gamma)-math.cos(alpha))/(v*math.sin(gamma)))
            c_frac = coordlist[j][2] * math.sin(gamma) / v
            outf.write('  {:>2s}'.format(atomnamelist[j]))
            outf.write('  {:>2s}'.format(atomnamelist[j]) + str(j) + "  1")
            outf.write('  {:>9.8f}'.format(a_frac / a_length))  # x componet only
            outf.write('  {:>9.8f}'.format(b_frac / b_length))  # y componet
            outf.write('  {:>9.8f}'.format(c_frac / c_length))  # z componet
            outf.write("  1\n")
```