2. RMP3 与 RMP4 能量#

创建时间:2019-11-01

这一节我们讨论 RMP3 与 RMP4 的能量计算。

读过 Szabo 第六章的人相信对 MPn 方法有所了解。不过我们这里不关注 MPn 方法的公式推导,并且将眼光局限于 Restricted 方法。事实上写这篇文档时,尽管曾经学习过一般的 MPn 与 CCPT 的推导方式,但并没有尝试推导过 Restricted 情况下的推导,仅仅是将书上出现的公式程序化而已。

这一篇文档的主要参考是 Helgaker et al. 2013 教材 [1] 的 section 14.4。关于 Spin-Orbital MP3 与 RMP3 的另一种实现方式,可以参考以 Szabo [2] 公式为蓝本的 Psi4NumPy 简要代码 [3]

import numpy as np
from pyscf import scf, gto

from functools import partial
np.einsum = partial(np.einsum, optimize=["greedy", 1024 ** 3 * 2 / 8])

import warnings
warnings.filterwarnings("ignore")

import re

np.set_printoptions(5, linewidth=150, suppress=True)

2.1. 分子体系与标准结果#

我们所使用的分子是非对称的双氧水分子,基组为 6-31G。比较常用但名称不太不常见的变量有

  • so, sv 表示占据轨道、非占轨道的分割 (split)

  • C, e 分别表示分子轨道系数 \(C_{\mu p}\) 与轨道能 \(\varepsilon_p\)

mol = gto.Mole()
mol.atom = """
O  0.0  0.0  0.0
O  0.0  0.0  1.5
H  1.0  0.0  0.0
H  0.0  0.7  1.0
"""
mol.basis = "6-31G"
mol.verbose = 0
mol.build()

natm = mol.natm
nao = nmo = mol.nao
nocc = mol.nelec[0]
nvir = nmo - nocc
so, sv = slice(0, nocc), slice(nocc, nmo)
scf_eng = scf.RHF(mol)
scf_eng.conv_tol = 1e-12
scf_eng.max_cycle = 128
scf_eng.kernel()
-150.5850337808384
C, e = scf_eng.mo_coeff, scf_eng.mo_energy
Co, Cv = C[:, so], C[:, sv]
eo, ev = e[so], e[sv]

我们的参考值来自于 Gaussian 的计算 (输入卡 H2O2-MP4.gjf、输出文件 H2O2-MP4.out)。

with open("H2O2-MP4.out", "r") as f:
    gaussian_output = f.readlines()

def gaussian_line(string):
    for line in gaussian_output:
        if string in line:
            return line[:-1].replace("D+", "E+").replace("D-", "E-")

其中一些结论有:

ref_mp2_corr = float(gaussian_line("E2 =").split()[2])
ref_mp3_corr = float(gaussian_line("E3=").split()[1])
ref_mp4_S = float(gaussian_line("MP4(S)=").split()[1])
ref_mp4_D = float(gaussian_line("MP4(D)=").split()[1])
ref_mp4_Q = float(gaussian_line("MP4(R+Q)=").split()[1])
ref_mp4_T = float(gaussian_line("E4(SDTQ)=").split()[1]) - float(gaussian_line("E4(SDQ)=").split()[1])
print("MP2 Corr: {:16.10f}".format(ref_mp2_corr))
print("MP3 Corr: {:16.10f}".format(ref_mp3_corr))
print("MP4 S   : {:16.10f}".format(ref_mp4_S))
print("MP4 D   : {:16.10f}".format(ref_mp4_D))
print("MP4 T   : {:16.10f}".format(ref_mp4_T))
print("MP4 Q   : {:16.10f}".format(ref_mp4_Q))
MP2 Corr:    -0.2690117593
MP3 Corr:     0.0074517392
MP4 S   :    -0.0043821604
MP4 D   :    -0.0088190269
MP4 T   :    -0.0067126030
MP4 Q   :     0.0011355775

2.2. RMP2 相关能量#

这里基本参照了 Helgaker 书的叙述思路,因此可能与我曾经写过的文档记号和变量名有微妙的区别。

变量名

公式表达式

意义

程序的角标顺序

出处

E_iajb

\(\varepsilon_{ij}^{ab}\)

轨道能差

\((i, a, j, b)\)

eq (14.4.18)

g_mo

\(g_{pqrs}\)

分子轨道 ERI 积分

\((p, q, r, s)\)

-

t_iajb

\({t_{ij}^{ab}}^{(1)}\)

一阶激发系数

\((i, a, j, b)\)

eq (14.4.41)

L_mo

\(L_{pqrs}\)

定义量

\((p, q, r, s)\)

eq (13.7.15)

T_iajb

\({\bar t_{ij}^{ab}}^{(1)}\)

定义量

\((i, a, j, b)\)

eq (14.4.42)

上述定义的一些实际表达式为

\[\begin{split} \begin{align} \varepsilon_{ij}^{ab} &= - \varepsilon_i + \varepsilon_a - \varepsilon_j + \varepsilon_b \\ g_{pqrs} &= C_{\mu p} C_{\nu q} (\mu \nu | \kappa \lambda) C_{\kappa r} C_{\lambda s} \\ {t_{ij}^{ab}}^{(1)} &= \frac{g_{iajb}}{\varepsilon_{ij}^{ab}} \\ L_{pqrs} &= 2 g_{pqrs} - g_{psrq} \\ {\bar t_{ij}^{ab}}^{(1)} &= 2 \frac{L_{iajb}}{\varepsilon_{ij}^{ab}} = 4 {t_{ij}^{ab}}^{(1)} - 2 {t_{ij}^{ba}}^{(1)} \end{align} \end{split}\]
eri_ao = mol.intor("int2e")
eri_mo = np.einsum("uvkl, up, vq, kr, ls -> pqrs", eri_ao, C, C, C, C)
E_iajb = - e[so, None, None, None] + e[None, sv, None, None] - e[None, None, so, None] + e[None, None, None, sv]
g_mo = eri_mo
t_iajb = - g_mo[so, sv, so, sv] / E_iajb
L_mo = 2 * g_mo - g_mo.swapaxes(-1, -3)
T_iajb = 4 * t_iajb - 2 * t_iajb.swapaxes(-1, -3)

在这些定义下,我们可以相当容易地写出 RMP2 的能量 (Helgaker, eq (14.4.55))

\[ E_\mathrm{RMP2, corr} = {t_{ij}^{ab}}^{(1)} L_{iajb} \]
energy_mp2_corr = (t_iajb * L_mo[so, sv, so, sv]).sum()
energy_mp2_corr
-0.26901177170795454

2.3. RMP3 相关能#

RMP3 能量可以根据 Helgaker, eq (14.4.61-62) 构建:

\[ X_{ij}^{ab} = \frac{1}{2} {t_{ij}^{cd}}^{(1)} g_{acbd} + \frac{1}{2} {t_{kl}^{ab}}^{(1)} g_{kilj} + {t_{ik}^{ac}}^{(1)} L_{bjkc} - {t_{kj}^{ac}}^{(1)} g_{bcki} - {t_{ki}^{ac}}^{(1)} g_{bjkc} \]
mp3_X_iajb = (
    + 0.5 * np.einsum("icjd, acbd -> iajb", t_iajb, g_mo[sv, sv, sv, sv])
    + 0.5 * np.einsum("kalb, kilj -> iajb", t_iajb, g_mo[so, so, so, so])
    + np.einsum("iakc, bjkc -> iajb", t_iajb, L_mo[sv, so, so, sv])
    - np.einsum("kajc, bcki -> iajb", t_iajb, g_mo[sv, sv, so, so])
    - np.einsum("kaic, bjkc -> iajb", t_iajb, g_mo[sv, so, so, sv])
)

关于 Helgaker 书上的 eq (14.4.59),可能是由于我们的实现与之实现方式不同,因此 Helgaker 书中的 \({\tilde t_{ij}^{ab}}^{(1)}\) 可以当作 \({\bar t_{ij}^{ab}}^{(1)}\) 使用。因此,

\[ E_\mathrm{RMP3, corr} = {\bar t_{ij}^{ab}}^{(1)} X_{ij}^{ab} \]
energy_mp3_corr = (T_iajb * mp3_X_iajb).sum()
energy_mp3_corr
0.007451743819516383

2.4. RMP4(SDQ) 相关能#

在 RMP4 相关能计算过程中,由于其中涉及到的三激发项从实现上较为复杂,因此我们将会对其分开讨论。

2.4.1. RMP4(S) 相关能#

定义以下变量:

变量名

公式表达式

意义

程序的角标顺序

出处

E_ia

\(\varepsilon_{i}^{a}\)

轨道能差

\((i, a)\)

eq (14.4.18)

t_2_ia

\({t_{i}^{a}}^{(2)}\)

二阶激发系数

\((i, a)\)

eq (14.4.50)

\(\varepsilon_i^a\) 的定义是显然的;\({t_{i}^{a}}^{(2)}\) 的定义为

\[ {t_{i}^{a}}^{(2)} = \frac{1}{\varepsilon_i^a} \left[ {t_{kl}^{ad}}^{(1)} L_{kild} - {t_{ki}^{cd}}^{(1)} L_{adkc} \right] \]

定义以下变量:

变量名

公式表达式

意义

程序的角标顺序

出处

E_ia

\(\varepsilon_{i}^{a}\)

轨道能差

\((i, a)\)

eq (14.4.18)

t_2_ia

\({t_{i}^{a}}^{(2)}\)

二阶激发系数

\((i, a)\)

eq (14.4.50)

E_ia = - e[so, None] + e[None, sv]
t_2_ia = (
    + np.einsum("kald, kild -> ia", t_iajb, L_mo[so, so, so, sv])
    - np.einsum("kcid, adkc -> ia", t_iajb, L_mo[sv, sv, so, sv])
)
t_2_ia /= E_ia

由此,根据 Helgaker eq (14.4.79) 与 eq (14.4.83),有

\[ S_{ij}^{ab} = {t_{j}^{c}}^{(2)} g_{aibc} - {t_{k}^{b}}^{(2)} g_{aikj} \]
mp4_S_iajb = (
    + np.einsum("jc, aibc -> iajb", t_2_ia, g_mo[sv, so, sv, sv])
    - np.einsum("kb, aikj -> iajb", t_2_ia, g_mo[sv, so, so, so])
)

以及

\[ E_\mathrm{RMP4, S} = {\bar t_{ij}^{ab}}^{(1)} S_{ij}^{ab} \]
energy_mp4_S = (T_iajb * mp4_S_iajb).sum()
energy_mp4_S
-0.004382160888639593

2.4.2. RMP4(D) 相关能#

定义以下变量:

变量名

公式表达式

意义

程序的角标顺序

出处

t_2_iajb

\({t_{ij}^{ab}}^{(2)}\)

二阶激发系数

\((i, a, j, b)\)

eq (14.4.51)

\({t_{ij}^{ab}}^{(2)}\) 的定义为

\[ {t_{ij}^{ab}}^{(2)} = \frac{1}{\varepsilon_{ij}^{ab}} \left[ - {t_{ij}^{cd}}^{(1)} g_{acbd} - {t_{kl}^{ab}}^{(1)} g_{kilj} - \hat P_{ij}^{ab} \left( {t_{ik}^{ac}}^{(1)} L_{bjkc} - {t_{kj}^{ac}}^{(1)} g_{bcki} - {t_{ki}^{ac}}^{(1)} g_{bjkc} \right) \right] \]

注意到这里的算符 \(\hat P_{ij}^{ab}\) 是对称化算符,其出处是 Helgaker eq (13.7.13):

\[ \hat P_{ij}^{ab} = A_{ij}^{ab} + A_{ji}^{ba} \]

为此,我们将 \({t_{ij}^{ab}}^{(2)}\) 的后一个关于 \((i, a, j, b)\) 的张量 \(\left( {t_{ik}^{ac}}^{(1)} L_{bjkc} - {t_{kj}^{ac}}^{(1)} g_{bcki} - {t_{ki}^{ac}}^{(1)} g_{bjkc} \right)\) 先使用变量 tmp_symm 储存,随后使用 np.transpose 转置来执行对称化。

tmp_symm = (
    + np.einsum("iakc, bjkc -> iajb", t_iajb, L_mo[sv, so, so, sv])
    - np.einsum("kajc, bcki -> iajb", t_iajb, g_mo[sv, sv, so, so])
    - np.einsum("kaic, bjkc -> iajb", t_iajb, g_mo[sv, so, so, sv])
)
tmp_symm += tmp_symm.transpose((2, 3, 0, 1))
t_2_iajb = (
    - np.einsum("icjd, acbd -> iajb", t_iajb, g_mo[sv, sv, sv, sv])
    - np.einsum("kalb, kilj -> iajb", t_iajb, g_mo[so, so, so, so])
    - tmp_symm
)
t_2_iajb /= E_iajb

由此,根据 Helgaker eq (14.4.80) 与 eq (14.4.83),有

\[ D_{ij}^{ab} = \frac{1}{2} {t_{ij}^{cd}}^{(2)} g_{acbd} + \frac{1}{2} {t_{kl}^{ab}}^{(2)} g_{kilj} + {t_{ik}^{ac}}^{(2)} L_{bjkc} - {t_{kj}^{ac}}^{(2)} g_{bcki} - {t_{ki}^{ac}}^{(2)} g_{bjkc} \]
mp4_D_iajb = (
    + 0.5 * np.einsum("icjd, acbd -> iajb", t_2_iajb, g_mo[sv, sv, sv, sv])
    + 0.5 * np.einsum("kalb, kilj -> iajb", t_2_iajb, g_mo[so, so, so, so])
    + 1.0 * np.einsum("iakc, bjkc -> iajb", t_2_iajb, L_mo[sv, so, so, sv])
    - 1.0 * np.einsum("kajc, bcki -> iajb", t_2_iajb, g_mo[sv, sv, so, so])
    - 1.0 * np.einsum("kaic, bjkc -> iajb", t_2_iajb, g_mo[sv, so, so, sv])
)

以及

\[ E_\mathrm{RMP4, D} = {\bar t_{ij}^{ab}}^{(1)} D_{ij}^{ab} \]
energy_mp4_D = (T_iajb * mp4_D_iajb).sum()
energy_mp4_D
-0.008819028049601586

2.4.3. RMP4(Q) 相关能#

在 Helgaker 的书中,RMP4(Q) 的相关能没有再作额外定义。根据 Helgaker eq (14.4.82) 与 eq (14.4.83),有

\[\begin{split} \begin{align} Q_{ij}^{ab} &= \frac{1}{2} {t_{kl}^{ab}}^{(1)} {t_{ij}^{cd}}^{(1)} g_{kcld} + {t_{ik}^{ac}}^{(1)} {t_{jl}^{bd}}^{(1)} L_{kcld} - {t_{ik}^{ac}}^{(1)} {t_{lj}^{bd}}^{(1)} L_{kcld} \\ & \quad + \frac{1}{2} {t_{ki}^{ac}}^{(1)} {t_{lj}^{bd}}^{(1)} g_{kcld} + \frac{1}{2} {t_{kj}^{ad}}^{(1)} {t_{li}^{bc}}^{(1)} g_{kcld} \\ & \quad - {t_{ik}^{ab}}^{(1)} {t_{lj}^{cd}}^{(1)} L_{lckd} - {t_{ij}^{ac}}^{(1)} {t_{kl}^{bd}}^{(1)} L_{kcld} \end{align} \end{split}\]
mp4_Q_iajb = (
    + 0.5 * np.einsum("kalb, icjd, kcld -> iajb", t_iajb, t_iajb, g_mo[so, sv, so, sv])
    + 1.0 * np.einsum("iakc, jbld, kcld -> iajb", t_iajb, t_iajb, L_mo[so, sv, so, sv])
    - 1.0 * np.einsum("iakc, lbjd, kcld -> iajb", t_iajb, t_iajb, L_mo[so, sv, so, sv])
    + 0.5 * np.einsum("kaic, lbjd, kcld -> iajb", t_iajb, t_iajb, g_mo[so, sv, so, sv])
    + 0.5 * np.einsum("kajd, lbic, kcld -> iajb", t_iajb, t_iajb, g_mo[so, sv, so, sv])
    - 1.0 * np.einsum("iakb, lcjd, lckd -> iajb", t_iajb, t_iajb, L_mo[so, sv, so, sv])
    - 1.0 * np.einsum("iajc, kbld, kcld -> iajb", t_iajb, t_iajb, L_mo[so, sv, so, sv])
)

以及

\[ E_\mathrm{RMP4, Q} = {\bar t_{ij}^{ab}}^{(1)} Q_{ij}^{ab} \]
energy_mp4_Q = (T_iajb * mp4_Q_iajb).sum()
energy_mp4_Q
0.001135577514293328

由此,我们可以计算 RMP4(SDQ) 所贡献的相关能:

energy_mp4SDQ_corr = energy_mp4_S + energy_mp4_D + energy_mp4_Q
energy_mp4SDQ_corr
-0.01206561142394785

2.5. RMP4(T) 相关能#

RMP4(T) 从实现上来讲,最简单的做法需要消耗 \(O^3 V^3\) 的内存。我们先从简单的实现入手;随后会简要给出一个中间张量的内存消耗是 \(O^2 V^2\) 的算法。

2.5.1. RMP4(T) 相关能:简单实现#

定义以下变量:

变量名

公式表达式

意义

程序的角标顺序

出处

E_iajbkc

\(\varepsilon_{ijk}^{abc}\)

轨道能差

\((i, a, j, b, k, c)\)

eq (14.4.18)

t_2_iajbkc

\({t_{ijk}^{abc}}^{(2)}\)

二阶激发系数

\((i, a, j, b, k, c)\)

eq (14.4.52)

\(\varepsilon_{ijk}^{abc}\) 的定义是显然的;\({t_{ijk}^{abc}}^{(2)}\) 的定义为

\[ {t_{ijk}^{abc}}^{(2)} = - \frac{1}{\varepsilon_{ijk}^{abc}} \hat P_{ijk}^{abc} \left( {t_{ij}^{ad}}^{(1)} g_{ckbd} - {t_{il}^{ab}}^{(1)} g_{cklj} \right) \]

这里的算符 \(\hat P_{ijk}^{abc}\) 仍然是对称算符,但其形式更复杂 (Helgaker, eq (13.7.14)):

\[ \hat P_{ijk}^{abc} A_{ijk}^{abc} = A_{ijk}^{abc} + A_{ikj}^{acb} + A_{jik}^{bac} + A_{jki}^{bca} + A_{kij}^{cab} + A_{kji}^{cba} \]
E_iajbkc = (
    - e[so, None, None, None, None, None]
    + e[None, sv, None, None, None, None]
    - e[None, None, so, None, None, None]
    + e[None, None, None, sv, None, None]
    - e[None, None, None, None, so, None]
    + e[None, None, None, None, None, sv]
)
t_2_iajbkc = (
    + np.einsum("iajd, ckbd -> iajbkc", t_iajb, g_mo[sv, so, sv, sv])
    - np.einsum("ialb, cklj -> iajbkc", t_iajb, g_mo[sv, so, so, so])
)
t_2_iajbkc = (
    + t_2_iajbkc.transpose((0, 1, 2, 3, 4, 5))
    + t_2_iajbkc.transpose((0, 1, 4, 5, 2, 3))
    + t_2_iajbkc.transpose((2, 3, 0, 1, 4, 5))
    + t_2_iajbkc.transpose((2, 3, 4, 5, 0, 1))
    + t_2_iajbkc.transpose((4, 5, 0, 1, 2, 3))
    + t_2_iajbkc.transpose((4, 5, 2, 3, 0, 1))
)
t_2_iajbkc = - t_2_iajbkc / E_iajbkc

由此,根据 Helgaker eq (14.4.81) 与 eq (14.4.83),有

\[ T_{ij}^{ab} = {t_{ijk}^{acd}}^{(2)} L_{bckd} - {t_{kji}^{acd}}^{(2)} g_{kdbc} - {t_{ikl}^{abc}}^{(2)} L_{kjlc} + {t_{lki}^{abc}}^{(2)} g_{kjlc} \]
mp4_T_iajb = (
    + np.einsum("iajckd, bckd -> iajb", t_2_iajbkc, L_mo[sv, sv, so, sv])
    - np.einsum("kajcid, kdbc -> iajb", t_2_iajbkc, g_mo[so, sv, sv, sv])
    - np.einsum("iakblc, kjlc -> iajb", t_2_iajbkc, L_mo[so, so, so, sv])
    + np.einsum("lakbic, kjlc -> iajb", t_2_iajbkc, g_mo[so, so, so, sv])
)

以及

\[ E_\mathrm{RMP4, T} = {\bar t_{ij}^{ab}}^{(1)} T_{ij}^{ab} \]
energy_mp4_T = (T_iajb * mp4_T_iajb).sum()
energy_mp4_T
-0.006712603570763939

由此,我们可以给出完整的 RMP4 相关能矫正了:

energy_mp4_corr = energy_mp4_S + energy_mp4_D + energy_mp4_T + energy_mp4_Q
energy_mp4_corr
-0.018778214994711787

2.5.2. RMP4(T) 相关能:中间张量 \(O^2 V^2\) 内存实现#

这里我们通过限制 \({t_{ijk}^{abc}}^{(2)}\) 中的 \(b\)\(k\) 并作求和的方式,将中间矩阵的储存大小降为 \(O^2 V^2\) 并给出 RMP4(T) 的能量。

这个严格来说还是至少消耗了 \(O^1 V^3\) 的内存,因为在计算过程中用到了张量 \(g_{aicd}\)

首先指出,像上述代码中使用 np.transpose 的方式来处理 \(\hat P_{ijk}^{abc}\) 在这里可能不适用;因此需要手输所有的中间张量的缩并过程。下述函数可以用来辅助我们进行带有指标转换的张量缩并。

# https://stackoverflow.com/a/11122744/9647779
def substitute_string(string, rule):
    str_lst = list(string)
    rule1, rule2 = rule.replace(" ", "").split("->")
    idx_list = [[i.start() for i in re.finditer(c, string)] for c in rule1]
    for idx, pos_list in enumerate(idx_list):
        for pos in pos_list:
            str_lst[pos] = rule2[idx]
    return "".join(str_lst)

譬如我们现在需要将缩并的目标 \({t_{ij}^{ad}}^{(1)} g_{ckbd}\)\((i, a, j, b, k, c)\) 转换为 \((j, b, k, c, i, a)\),那么我们执行下述代码就可以生成转换后的缩并字符串了:

substitute_string("iajd, ckbd", "iajbkc -> jbkcia")
'jbkd, aicd'

后文中出现的代码就是受这个小函数的帮助而写成的。

事实上,下述程序几乎与上面的程序等价,只是每次处理 \({t_{ijk}^{abc}}^{(2)}\) 张量时,总是先固定 \(b, k\) 指标,而对其它的张量进行正常的运算,求出固定了 \(b, k\) 的 RMP4(T) 相关能;随后我们再对 \(b, k\) 指标进行循环,把所有 \(b, k\) 相关能贡献总和起来,得到最终的 RMP4(T) 相关能。

energy_mp4_T_by_ckiter = 0
for k in range(nocc):
    for b in range(nvir):
        bn = b + nocc
        t_2_bk_iajb = - (
            + np.einsum("iajd, cd -> iajc", t_iajb[:, :, :, :], g_mo[sv,  k, bn, sv])
            - np.einsum("ial, clj -> iajc", t_iajb[:, :, :, b], g_mo[sv,  k, so, so])
            + np.einsum("iad, jcd -> iajc", t_iajb[:, :, k, :], g_mo[bn, so, sv, sv])
            - np.einsum("ialc, jl -> iajc", t_iajb[:, :, :, :], g_mo[bn, so, so,  k])
            + np.einsum("jid, cad -> iajc", t_iajb[:, b, :, :], g_mo[sv,  k, sv, sv])
            - np.einsum("jla, cli -> iajc", t_iajb[:, b, :, :], g_mo[sv,  k, so, so])
            + np.einsum("jd, aicd -> iajc", t_iajb[:, b, k, :], g_mo[sv, so, sv, sv])
            - np.einsum("jlc, ail -> iajc", t_iajb[:, b, :, :], g_mo[sv, so, so,  k])
            + np.einsum("cid, jad -> iajc", t_iajb[k, :, :, :], g_mo[bn, so, sv, sv])
            - np.einsum("cla, jli -> iajc", t_iajb[k, :, :, :], g_mo[bn, so, so, so])
            + np.einsum("cjd, aid -> iajc", t_iajb[k, :, :, :], g_mo[sv, so, bn, sv])
            - np.einsum("cl, ailj -> iajc", t_iajb[k, :, :, b], g_mo[sv, so, so, so])
        )
        t_2_bk_iajb /= (
            - e[so, None, None, None]
            + e[None, sv, None, None]
            - e[None, None, so, None]
            + e[None, None, None, sv]
            - e[k] + e[bn]
        )
        mp4_T_bk_iajb = (
            + np.einsum("iajd, bd -> iajb", t_2_bk_iajb, L_mo[sv, bn,  k, sv])
            - np.einsum("idja, db -> iajb", t_2_bk_iajb, g_mo[ k, sv, sv, bn])
            - np.einsum("ialb, jl -> iajb", t_2_bk_iajb, L_mo[ k, so, so, bn])
            + np.einsum("laib, jl -> iajb", t_2_bk_iajb, g_mo[ k, so, so, bn])
        )
        energy_mp4_T_by_ckiter += (T_iajb * mp4_T_bk_iajb).sum()
energy_mp4_T_by_ckiter
-0.006712603570763938