前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用四元数计算两个分子之间的RMSD(附Python代码)

使用四元数计算两个分子之间的RMSD(附Python代码)

作者头像
用户7592569
发布2022-12-07 15:06:48
2.7K0
发布2022-12-07 15:06:48
举报
文章被收录于专栏:量子化学量子化学

本文将简要介绍如何使用四元数方法计算两个分子之间RMSD,同时附上简单的示例Python代码。

1. 什么是RMSD

RMSD(Root Mean Square Deviation)是指均方根偏差,在化学中一般用于衡量一个分子结构相对于参照分子的原子偏离位置。RMSD的值越小,说明当前分子结构越接近参照的分子结构。RMSD的数学定义为[1]:

\begin{aligned} RMSD(\mathbf{v},\mathbf{w}) & =\sqrt{\frac{1}{N}\sum_{i=1}^{N}\lVert v_i-w_i\rVert^2} \\ & =\sqrt{\frac{1}{N}\sum_{i=1}^{N}((v_{ix}-w_{ix})^2+(v_{iy}-w_{iy})^2+(v_{iz}-w_{iz})^2)} \end{aligned} \tag{2}

从上式也可以看出,当两个分子的几何结构完全一样时,RMSD的值是0。

在量子化学中,xyz文件是一种比较通用的记录分子几何结构的文件格式,其内容如下:

代码语言:javascript
复制
1 原子数量
2 标题
3 原子1  x1 y1 z1
4 原子2  x2 y2 z2
5 原子3  x3 y3 z3
…………

上面每行开头的数字为行数的辅助标记,不在xyz文件中出现。我们的目标是使用四元数方法,写出一个可以计算A、B两个分子之间RMSD值的Python脚本rmsd.py,即在给出两个坐标文件a.xyz和b.xyz后,输入如下命令:

代码语言:javascript
复制
$ ./rmsd.py a.xyz b.xyz
$ rmsd: x.xxxxxx

即可得到A、B分子之间的RMSD值。

2. 基本思路

RMSD的计算公式很简单,主要难点在于怎样将两个分子放在尽可能”相近“的位置上计算。换言之,RMSD会随着两个分子的相对位置变化而变化,我们需要找到RMSD最小的时候对应的相对位置。举个例子,假设我们有两个水分子1.xyz和2.xyz:

代码语言:javascript
复制
1.xyz

3
H2O-1
H                 -0.78397589     0.44324751   0.00000000
O                  0.00000000    -0.11081188   0.00000000
H                  0.78397589     0.44324751   0.00000000
代码语言:javascript
复制
2.xyz

3
H2O-2
H                 -0.81664155    0.46171616   0.00000000
O                  0.00000000   -0.11542904   0.00000000
H                  0.81664155    0.46171616   0.00000000

上面第二个水分子的键长做了微小调整,根据公式2可得出1.xyz和2.xyz的RMSD值为0.307549。假如我们对第二个水分子做一些平动和转动(为保持直观感受,假设两个分子都固定在xy平面),那么RMSD的值就会发生改变:

除了平动和转动会影响RMSD,原子之间的编号顺序也会产生影响,比如下图:

假设四个灰色原子是同样类型的原子,如果将2、3号原子的编号交换然后和未交换编号的分子做比较,RMSD就会从0变为1.088944。

由此我们可以看出,在计算两个分子RMSD值之前,还至少需要四个步骤:确认两个分子的原子类型和数量相等、优化同类原子的编号顺序、优化分子的平动和优化分子的转动。

3. 什么是四元数

我们首先看看四元数是什么。四元数和复数类似,可以表示成如下形式[2]:

a=q_0+q_1\mathbf{i}+q_2\mathbf{j}+q_3\mathbf{k} \tag{3}

其中i、j、k满足如下乘法表:

q=(q_0, q_1, q_2, q_3)=(q_0, \mathbf{q}) =\mathbf{Q}

四元数的一些基本运算:

a+b=(a_0+b_0, \mathbf{a}+\mathbf{b})
ab=(a_0b_0-\mathbf{a}\cdot\mathbf{b}, a_0\mathbf{b}+b_0\mathbf{a}+\mathbf{a}\times\mathbf{b})
ab=(a_0b_0-\mathbf{a}\cdot\mathbf{b}, a_0\mathbf{b}+b_0\mathbf{a}+\mathbf{a}\times\mathbf{b})
ab=(a_0b_0-\mathbf{a}\cdot\mathbf{b}, a_0\mathbf{b}+b_0\mathbf{a}+\mathbf{a}\times\mathbf{b})
a^c=(a_0, -\mathbf{a})
a^c=-a
ab=(-\mathbf{a}\cdot\mathbf{b},\mathbf{a}\times\mathbf{b})
ab+ba=2(-\mathbf{a}\cdot\mathbf{b},\mathbf{0})

注意四元数的乘法不满足交换律,但是满足结合律和分配律。此外,四元数的乘法运算也可以写成矩阵的形式[3]:

ab=\mathbf{A}_L(a)\mathbf{Q}
ba=\mathbf{A}_R(a)\mathbf{Q}
\mathbf{A}_L(a)= \begin{pmatrix} a_0 & -a_1 & -a_2 & -a_3 \\ a_1 & a_0 & -a_3 & a_2 \\ a_2 & a_3 & a_0 & -a_1 \\ a_3 & -a_2 & a_1 & a_0 \end{pmatrix} = \begin{pmatrix} a_0 & \mathbf{-a} \\ \mathbf{a^T} & a_0\mathbf{I}+\mathbf{K(r)} \\ \end{pmatrix}
\mathbf{A}_L(a)= \begin{pmatrix} a_0 & -a_1 & -a_2 & -a_3 \\ a_1 & a_0 & -a_3 & a_2 \\ a_2 & a_3 & a_0 & -a_1 \\ a_3 & -a_2 & a_1 & a_0 \end{pmatrix} = \begin{pmatrix} a_0 & \mathbf{-a} \\ \mathbf{a^T} & a_0\mathbf{I}+\mathbf{K(r)} \\ \end{pmatrix}
\mathbf{A}_R(a)= \begin{pmatrix} a_0 & -a_1 & -a_2 & -a_3 \\ a_1 & a_0 & a_3 & -a_2 \\ a_2 & -a_3 & a_0 & a_1 \\ a_3 & a_2 & -a_1 & a_0 \end{pmatrix} = \begin{pmatrix} a_0 & \mathbf{-a} \\ \mathbf{a^T} & a_0\mathbf{I}-\mathbf{K(r)} \\ \end{pmatrix}
\mathbf{A}_R(a)= \begin{pmatrix} a_0 & -a_1 & -a_2 & -a_3 \\ a_1 & a_0 & a_3 & -a_2 \\ a_2 & -a_3 & a_0 & a_1 \\ a_3 & a_2 & -a_1 & a_0 \end{pmatrix} = \begin{pmatrix} a_0 & \mathbf{-a} \\ \mathbf{a^T} & a_0\mathbf{I}-\mathbf{K(r)} \\ \end{pmatrix}
\mathbf{K(r)}= \begin{pmatrix} 0 & -a_3 & a_2 \\ a_3 & 0 & -a_1 \\ -a_2 & a_1 & 0 \end{pmatrix}
r^{\prime}=(0,\mathbf{r^{\prime}}),\ r=(0,\mathbf{r}),\ r^{\prime}=qrq^c
(0,\mathbf{r^{\prime}})=(q_0,\mathbf{q})(0,\mathbf{r})(q_0,-\mathbf{q})=(0,\mathbf{U}(q)\mathbf{r})
(0,\mathbf{r^{\prime}})=(q_0,\mathbf{q})(0,\mathbf{r})(q_0,-\mathbf{q})=(0,\mathbf{U}(q)\mathbf{r})
\begin{pmatrix} 1 & \mathbf{0^T} \\ \mathbf{0} & \mathbf{U}(q) \end{pmatrix} = \mathbf{A}_L(q)\mathbf{A}_R(q^c) = \mathbf{A^T}_R(q)\mathbf{A}_L(q)
\begin{pmatrix} 1 & \mathbf{0^T} \\ \mathbf{0} & \mathbf{U}(q) \end{pmatrix} = \mathbf{A}_L(q)\mathbf{A}_R(q^c) = \mathbf{A^T}_R(q)\mathbf{A}_L(q)

4. 开始计算RMSD

我们通过xyz文件读取原子坐标的信息,所以可以先写一个简单的xyz文件解析器:

代码语言:javascript
复制
# xyz file parser
def parse_xyz(_xyz):

    # check number of atoms
    atom_number = 0

    # line status
    line_number = 0

    # buffer
    atom_name_buffer = []
    atom_coord_buffer = []

    with open(_xyz, "r") as xyz_file:
        for line in xyz_file:
            line_number += 1

            # the first line: number of atoms
            if (line_number == 1):
                if (len(line.split()) == 1):
                    atom_number = int(line.split()[0])
                else:
                    print("parse xyz file failed: line ", line_number)
                    exit()

            # the second line: ignore title
            if (line_number == 2):
                pass

            # other lines: atom names and coordinates
            if (line_number > 2):
                if (len(line.split()) == 4):
                    atom_name_buffer.append(str(line.split()[0]))
                    line_buffer = [0.0, 0.0, 0.0]
                    line_buffer[0] = float(line.split()[1])
                    line_buffer[1] = float(line.split()[2])
                    line_buffer[2] = float(line.split()[3])
                    atom_coord_buffer.append(line_buffer)
                else:
                    print("parse xyz file failed: line ", line_number)
                    exit()

    # check
    if (not (len(atom_name_buffer) == (len(atom_coord_buffer)) == atom_number)):
        print("wrong atom number")
        exit()

    # change to numpy array after check
    atom_name = numpy.array(atom_name_buffer)
    atom_coord = numpy.array(atom_coord_buffer)

    return atom_name, atom_coord
代码语言:javascript
复制
def main():
    # 1. arguments check
    if (len(sys.argv) != 3):
        print("wrong number of arguments")
        print("usage: ./rmsd.py [a.xyz] [b.xyz]")
        exit()
        
    # 2. parse xyz files
    a_xyz_file = sys.argv[1]
    b_xyz_file = sys.argv[2]
    a_atom, a_coord = parse_xyz(a_xyz_file)
    b_atom, b_coord = parse_xyz(b_xyz_file)
    
if __name__ == "__main__":
    main()

为了给原子编号优化和转动矩阵计算做准备,我们首先把两个分子的几何中心全都移动到坐标原点(后面会解释为什么),并且添加函数调用:

代码语言:javascript
复制
def centroid_translation(_atom_coord):
    centroid = _atom_coord.mean(axis=0)
    _atom_coord -= centroid
    return _atom_coord
    
def main():
    # 1. arguments check
    ......
    # 2. parse xyz files
    ......
    # 3. move centroid
    a_coord = centroid_translation(a_coord)
    b_coord = centroid_translation(b_coord)

现在我们的两个分子的几何中心已经重合,并且都在坐标原点。接下来我们要进行第一个优化步骤,尽可能对齐两个分子的原子编号,也就是纠正第2节中图2的那种编号错位。对齐原子编号可以使用匈牙利算法(Hungarian algorithm),匈牙利算法所解决的问题可以抽象为如下数学模型[5]:假设M个行指标和N列指标可以组成一个矩阵

\mathbf{C}= \begin{pmatrix} c_{11} & c_{12} & \cdots & c_{1N} \\ c_{21} & c_{22} & \cdots & c_{2N} \\ \vdots & \vdots & \ddots & \vdots \\ c_{M1} & c_{M2} & \cdots & c_{MN} \\ \end{pmatrix}

其中每个矩阵元表示一对横纵坐标之间的“成本函数”,我们通过更改行或列指标的“分配”,使得最后的“总成本”降到最低。从数学上讲我们是计算了这样一个函数:

min\sum_i\sum_jC_{ij}X_{ij}
代码语言:javascript
复制
def reorder(_a_atom, _a_coord, _b_atom, _b_coord):

    # result reorder index container
    reorder_indx = numpy.zeros(_b_atom.shape, dtype=int)

    # reordered by atom type
    unique_atom = numpy.unique(_a_atom)
    for atom in unique_atom:
        a_atom_indx, = numpy.where(_a_atom == atom)
        b_atom_indx, = numpy.where(_b_atom == atom)

        # Hungarian assignment, cost matrix is distance matrix
        a_atom_coord = _a_coord[a_atom_indx]
        b_atom_coord = _b_coord[b_atom_indx]
        distance_cost = scipy.spatial.distance.cdist(a_atom_coord, b_atom_coord)
        a_indx, b_reorder_indx = scipy.optimize.linear_sum_assignment(
            distance_cost)

        # unique atom reorder index
        reorder_indx[a_atom_indx] = b_atom_indx[b_reorder_indx]

    # reorder operation
    _b_atom = _b_atom[reorder_indx]
    _b_coord = _b_coord[reorder_indx]

    return _b_atom, _b_coord

上面代码通过scipy.spatial.distance.cdist函数计算“成本矩阵”,使用scipy.optimize.linear_sum_assignment函数进行指标分配。此外,在上面的计算中,我们是在同类型原子之间进行编号优化,这也很好理解,比如对于甲烷分子,把C原子和H原子进行编号交换是不合理的。

接下来就到了四元数参与的部分了[3]。在读取原子坐标、对齐几何中心、对齐原子编号完成之后,接下来要计算平动-转动矩阵。从数学上讲,我们是要最小化这样一个函数:

E=\frac{1}{N}\sum_{k=1}^{N}|\mathbf{Ux}_k+\mathbf{r}-\mathbf{y}_k|^2
\sum_{k=1}^{N}(\mathbf{Ux}_k+\mathbf{r}-\mathbf{y}_k)=0
\mathbf{r}=\overline{y}-\mathbf{U}\overline{x}=\frac{1}{N}\sum_{k=1}^{N}\mathbf{y}_k-\mathbf{U}\frac{1}{N}\sum_{k=1}^{N}\mathbf{x_k}
\tilde{\mathbf{x}}=\mathbf{x}_k-\overline{\mathbf{x}}, \tilde{\mathbf{y}}=\mathbf{y}_k-\overline{\mathbf{y}}

原函数将简化为:

E=\frac{1}{N}\sum_{k=1}^{N}|\mathbf{U\tilde{x}}_k-\mathbf{\tilde{y}}_k|^2
(0,\mathbf{U}(q))=qx_kq^c

求极小值的函数可以写为:

E_q=\frac{1}{N}\sum_{k=1}^{N}(qx_kq^c-y_k)(qx_kq^c-y_k)^c
NE_q=\sum_{k}^{N}((qx_kq^c)(qx_kq^c)^c)+y_ky_k^c-(qx_kq^c)y_k^c-y_k(qx_kq^c)^c)
NE_q=\sum_{k}^{N}(x_kx_k^c+y_ky_k^c-(qx_kq^c)y_k^c-y_k(qx_kq^c)^c) =\sum_{k=1}^{N}(|\mathbf{x}_k|^2+|\mathbf{y}_k|^2+(qx_kq^c)y_k+y_k(qx_kq^c))

需要优化的部分还剩下

(qx_kq^c)y_k+y_k(qx_kq^c)

这一项,通过第二节中纯四元数性质

ab+ba=2(-\mathbf{a}\cdot\mathbf{b},\mathbf{0})

代入可得:

(qx_kq^c)y_k+y_k(qx_kq^c)=2([(y_kqx_k)q^c]_0, \mathbf{0})
[(y_kqx_k)q^c]_0=\mathbf{Q^T}\mathbf{A}_L(y_k)\mathbf{A}_R(x_k)\mathbf{Q}
NE_q=\sum_{k=1}^{N}(|\mathbf{x}_k|^2+|\mathbf{y}_k|^2)-2\mathbf{Q^T}\mathbf{A^T}_L(y_k)\mathbf{A}_R(x_k)\mathbf{Q}
代码语言:javascript
复制
# generate A_L matrix
def A_L_matrix(q1, q2, q3, q4=0):
    A_L = numpy.array([
        [q4, -q3, q2, q1],
        [q3, q4, -q1, q2],
        [-q2, q1, q4, q3],
        [-q1, -q2, -q3, q4],
    ])
    return A_L
    
# generate A_R matrix
def A_R_matrix(q1, q2, q3, q4=0):
    A_R = numpy.array([
        [q4, q3, -q2, q1],
        [-q3, q4, q1, q2],
        [q2, -q1, q4, q3],
        [-q1, -q2, -q3, q4],
    ])
    return A_R
代码语言:javascript
复制
def quaternion_rotation(_a_coord, _b_coord):
    # generate A matrix
    N = _a_coord.shape[0]
    A = numpy.zeros([4, 4], dtype=float)
    for k in range(N):
        A_L = A_L_matrix(_a_coord[k, 0], _a_coord[k, 1], _a_coord[k, 2])
        A_R = A_R_matrix(_b_coord[k, 0], _b_coord[k, 1], _b_coord[k, 2])
        A_Lt_dot_A_R = numpy.dot(A_L.transpose(), A_R)
        A += A_Lt_dot_A_R

    # generate rotation matrix
    eigen = numpy.linalg.eigh(A)
    Q = eigen[1][:, eigen[0].argmax()]
    A_Rt_rot = A_R_matrix(*Q).transpose()
    A_L_rot = A_L_matrix(*Q)
    rot = numpy.dot(A_Rt_rot, A_L_rot)[:3, :3]

    # rotate coordinates: rotate _a_coord because U(q)x_k from the equation
    _a_coord = numpy.dot(_a_coord, rot)

    return _a_coord

最后一步,计算坐标变换之后的RMSD,所有步骤就全都完成了:

代码语言:javascript
复制
def rmsd(_a_coord, _b_coord):
    diff = numpy.array(_a_coord) - numpy.array(_b_coord)
    N = numpy.array(_a_coord).shape[0]
    result = numpy.sqrt((diff * diff).sum() / N)
    return result

上面提供了计算RMSD的一种方法,适用于分子结构较为简单、RMSD较小的情况。对于复杂分子、结构差别较大或含有手性中心等特殊情况,则需要额外引入其他计算方法[6]。

参考文献

[1] https://en.wikipedia.org/wiki/Root-mean-square_deviation_of_atomic_positions

[2] https://en.wikipedia.org/wiki/Quaternion

[3] Coutsias E. A.; Seok C.; Dill K. A. Using Quaternions to Calculate RMSD. J. Comput. Chem. 2004, 25, 1849-1857.

[4] Walker M. W.; Shao L.; Volz R. A. Estimating 3-D location parameters using dual number quaternions. CVGIP: Image Understanding. 1991, 54, 358-367.

[5] https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html

[6] https://github.com/charnley/rmsd(参考了部分代码)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 量子化学 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 什么是RMSD
  • 2. 基本思路
  • 3. 什么是四元数
  • 4. 开始计算RMSD
    • 参考文献
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档