```python
warning_status = "ignore" #@param ["ignore", "always", "module", "once", "default", "error"]
import warnings
warnings.filterwarnings(warning_status)
with warnings.catch_warnings():
    warnings.filterwarnings(warning_status, category=DeprecationWarning)
    warnings.filterwarnings(warning_status, category=UserWarning)

import numpy as np
import os
#@markdown This sets the styles of the plotting (default is styled like plots from [FiveThirtyeight.com](https://fivethirtyeight.com/)
matplotlib_style = 'fivethirtyeight' #@param ['fivethirtyeight', 'bmh', 'ggplot', 'seaborn', 'default', 'Solarize_Light2', 'classic', 'dark_background', 'seaborn-colorblind', 'seaborn-notebook']
import matplotlib.pyplot as plt; plt.style.use(matplotlib_style)
import matplotlib.axes as axes;
from matplotlib.patches import Ellipse
#%matplotlib inline
import seaborn as sns; sns.set_context('notebook')

import torch
import torch.nn.functional as F
import pyro
import pyro.poutine as poutine
import pyro.distributions as dist
from pyro.infer import MCMC, NUTS
from pyro.infer.mcmc import MCMC
from pyro.infer.mcmc.nuts import HMC
from pyro.infer import EmpiricalMarginal
from tqdm import tqdm
#@markdown This sets the resolution of the plot outputs (`retina` is the highest resolution)
notebook_screen_res = 'retina' #@param ['retina', 'png', 'jpeg', 'svg', 'pdf']
#%config InlineBackend.figure_format = notebook_screen_res

%load_ext autoreload
%autoreload 2
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
class _PlotColor(object):
    """Enum of colors """
    red = '#F15854'
    blue = '#5DA5DA'
    orange = '#FAA43A'
    green = '#60BD68'
    pink = '#F17CB0'
    brown = '#B2912F'
    purple = '#B276B2'
    yellow = '#DECF3F'
    gray = '#4D4D4D'
    def __getitem__(self, i):
        return [
            self.red,
            self.orange,
            self.green,
            self.blue,
            self.pink,
            self.brown,
            self.purple,
            self.yellow,
            self.gray,
        ][i % 9]
PlotColor = _PlotColor()
```

#### Coin Flip Example

```python

num_trials = torch.tensor([0,1, 2, 3, 4, 5, 8, 15, 50, 500, 1000, 2000])
coin_flip_prior = pyro.distributions.Bernoulli(probs=torch.ones(num_trials[-1]) *0.5)
coin_flip_data = pyro.sample('coin_flip', coin_flip_prior)

# pad the left side with 0 for the 0th flip
coin_flip_sequence = F.pad(coin_flip_data, (1, 0), value=0)
# cumulative counts of heads
coin_flip_scores = torch.cumsum(coin_flip_sequence, 0)[num_trials]

```

```python
rv_observed_heads = pyro.distributions.Beta(
    concentration1=torch.tensor(1 + coin_flip_scores).double(),
    concentration0=torch.tensor(1 + num_trials - coin_flip_scores).double())

```

```python
# For the already prepared, I'm using Binomial's conj. prior.
probs_of_heads = torch.linspace(start=0., end=1., steps=100)
observed_probs_heads = torch.exp(rv_observed_heads.log_prob(probs_of_heads[:, None]))
plt.figure(figsize=(16, 9))
for i in range(len(num_trials)):
    sx = plt.subplot(len(num_trials)/2, 2, i+1)
    plt.xlabel("$p$, probability of heads") \
    if i in [0, len(num_trials)-1] else None
    plt.setp(sx.get_yticklabels(), visible=False)
    plt.plot(probs_of_heads, observed_probs_heads[:, i],
             label="observe %d tosses,\n %d heads" % (num_trials[i], coin_flip_scores[i]))
    plt.fill_between(probs_of_heads, 0, observed_probs_heads[:, i],
                     color='#5DA5DA', alpha=0.4)
    plt.vlines(0.5, 0, 4, color="k", linestyles="--", lw=1)
    leg = plt.legend()
    leg.get_frame().set_alpha(0.4)
    plt.autoscale(tight=True)

plt.suptitle("Bayesian updating of posterior probabilities", y=1.02,
             fontsize=14)
plt.tight_layout()
```

### 1.4 Text message data

```python
count_data = torch.tensor([
    13,  24,   8,  24,   7,  35,  14,  11,  15,  11,  22,  22,  11,  57,
    11,  19,  29,   6,  19,  12,  22,  12,  18,  72,  32,   9,   7,  13,
    19,  23,  27,  20,   6,  17,  13,  10,  14,   6,  16,  15,   7,   2,
    15,  15,  19,  70,  49,   7,  53,  22,  21,  31,  19,  11,  18,  20,
    12,  35,  17,  23,  17,   4,   2,  31,  30,  13,  27,   0,  39,  37,
    5,  14,  13,  22,
], dtype=torch.float32)
n_count_data = len(count_data)
days = torch.range(start=0, end=n_count_data -1 )

# Visualizing the Results
plt.figure(figsize=(12.5, 4))
plt.bar(days.numpy().squeeze(), count_data.numpy().squeeze(), color="#5DA5DA")
plt.xlabel("Time (days)")
plt.ylabel("count of text-msgs received")
plt.title("Did the user's texting habits change over time?")
plt.xlim(0, n_count_data);
```

We model the text msg count as Poission distribution (defined by $\lambda$) , with a switchover point.

Each $\lambda$ is parameterized by $\alpha$
$$\lambda =
\begin{cases} \lambda_1  & \text{if } t \lt \tau \cr
\lambda_2 & \text{if } t \ge \tau
\end{cases}
$$

$$
\begin{align}
&\lambda_1 \sim \text{Exp}( \alpha_1 ) \\
&\lambda_2 \sim \text{Exp}( \alpha_2 )
\end{align}
$$

```python
def model(data):
    alpha = (1. / data.mean())
    lambda1 = pyro.sample("lambda1", dist.Exponential(rate=alpha))
    lambda2 = pyro.sample("lambda2", dist.Exponential(rate=alpha))

    tau = pyro.sample("tau", dist.Uniform(0, 1))
    lambda1_size = (tau * data.size(0) + 1).long()
    lambda2_size = data.size(0) - lambda1_size
    lambda_ = torch.cat([lambda1.expand((lambda1_size,)), lambda2.expand((lambda2_size,))])

    with pyro.plate("data", data.size(0)):
        pyro.sample("obs", dist.Poisson(lambda_), obs=data)
hmc_kernel = HMC(model, jit_compile=True, step_size=0.0855, num_steps=4)
nuts_kernel = NUTS(model, jit_compile=True, max_tree_depth=2, adapt_step_size=True,)
mcmc = MCMC(nuts_kernel, num_samples=10000, warmup_steps=5000, num_chains=1)
mcmc.run(count_data)

lambda_1_samples = mcmc.get_samples()["lambda1"]
lambda_2_samples = mcmc.get_samples()["lambda2"]
tau_samples = mcmc.get_samples()["tau"]

```

    Sample: 100%|██████████| 15000/15000 [00:42, 352.57it/s, step size=3.72e-01, acc. prob=0.781]

```python
plt.figure(figsize=(12.5, 15))
#histogram of the samples:

ax = plt.subplot(311)
ax.set_autoscaley_on(False)

plt.hist(lambda_1_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label=r"posterior of $\lambda_1$", color=PlotColor[0], density=True)
plt.legend(loc="upper left")
plt.title(r"""Posterior distributions of the variables $\lambda_1,\;\lambda_2,\;\tau$""")
plt.xlim([15, 30])
plt.xlabel(r"$\lambda_1$ value")

ax = plt.subplot(312)
ax.set_autoscaley_on(False)
plt.hist(lambda_2_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label=r"posterior of $\lambda_2$", color=PlotColor[6], density=True)
plt.legend(loc="upper left")
plt.xlim([15, 30])
plt.xlabel(r"$\lambda_2$ value")

plt.subplot(313)
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
plt.hist((tau_samples * n_count_data).int(), bins=n_count_data, alpha=1,
         label=r"posterior of $\tau$",
         color=PlotColor[2], weights=w, rwidth=2.)
plt.xticks(np.arange(n_count_data))

plt.legend(loc="upper left")
plt.ylim([0, .75])
plt.xlim([35, len(count_data)-20])
plt.xlabel(r"$\tau$ (in days)")
plt.ylabel(r"probability");
```

```python
num_samples = tau_samples.shape[0]
expected_texts_per_day = np.zeros(n_count_data)
for day in tqdm(range(n_count_data)):
    indexes = torch.where(day < (tau_samples * n_count_data).int())[0].long()
    inverse_indexes = torch.where(day >= (tau_samples * n_count_data).int())[0].long()
    inverse_index[indexes] = 0
    inverse_index = inverse_index.bool()
    expected_texts_per_day[day] = (lambda_1_samples.index_select(0, indexes).sum() + lambda_2_samples.index_select(0, inverse_indexes).sum()) / num_samples
```

    100%|██████████| 74/74 [00:00<00:00, 6242.53it/s]

```python

plt.figure(figsize=(12.5, 9))
plt.plot(range(n_count_data), expected_texts_per_day, lw=4, color="#E24A33",
         label="expected number of text-messages received")
plt.xlim(0, n_count_data)
plt.xlabel("Day")
plt.ylabel("Expected # text-messages")
plt.title("Expected number of text-messages received")
plt.ylim(0, 60)
plt.bar(np.arange(len(count_data)), count_data, color="#5DA5DA", alpha=0.65,
        label="observed texts per day")

plt.legend(loc="upper left");
```

```python
lambda_1_samples
```

    tensor([18.5231, 18.8324, 18.6827,  ..., 18.2884, 18.3793, 18.3639])

### Exercises

Using lambda_1_samples and lambda_2_samples, what is the mean of the posterior distributions of 𝜆1 and 𝜆2?

```python
lambda_1_samples.mean(), lambda_2_samples.mean()
```

    (tensor(17.7559), tensor(22.6729))

What is the expected percentage increase in text-message rates? hint: compute the mean of lambda_1_samples/lambda_2_samples. Note that this quantity is very different from lambda_1_samples.mean()/lambda_2_samples.mean()

```python
percentage_increase_distribution = (lambda_2_samples - lambda_1_samples) / lambda_1_samples

plt.figure(figsize=(10, 5))

plt.hist(percentage_increase_distribution, histtype='stepfilled', bins=30, alpha=0.85,
         label=r"posterior of % increase", color=PlotColor[0], density=True)
plt.legend(loc="upper right")

plt.xlabel("% increase")
```

```python

```


```python
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
import math
from scipy import stats
#카이 제곱을 쓸수 있음
from ast import literal_eval
#파이썬 애플리케이션이 파이썬 추상 구문 문법 트리를 처리하는데 도움
from sklearn.metrics.pairwise import linear_kernel, cosine_similarity
#유사도, sklearn 머신러닝 도구를 사용
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
import warnings; warnings.simplefilter('ignore')
#warning 제거
```

```python

```

```python
person = pd.read_csv('personality.csv', encoding='utf-8').dropna(how='all').drop('score_date',1)
movie = pd.read_csv('movies.csv',encoding='cp949')

#train_person, test_person = train_test_split(person, test_size=0.2,shuffle=False,random_state=1004)
#train_person

kf = KFold(n_splits=5)
for train_person, test_person in kf.split(person):

        train = person.iloc[train_person]
        test = person.iloc[test_person]
        train_user_id = train['user_id'].drop_duplicates( keep='first', inplace=False ).reset_index(drop=True)
        test_user_id = test['user_id'].drop_duplicates( keep='first', inplace=False ).reset_index(drop=True)
        movie_person = pd.merge(train, movie, on="movie_id", how="left")
        movie_person = movie_person.dropna(axis=0)

        movie_person_score = movie_person.drop(['movie_name', 'openness', 'neuroticism', 'conscientiousness', 'agreeableness', 'extraversion', 'movie_name'], axis=1)
        user_id_list = list(movie_person_score.groupby('user_id'))

        movie_person1 = movie_person.drop(['user_id', 'movie_id', 'movie_score', 'movie_name', 'openness', 'neuroticism', 'conscientiousness', 'agreeableness', 'extraversion', 'movie_name'], axis=1)
        x = movie_person1.values

        person1 = movie_person[['openness', 'neuroticism', 'conscientiousness', 'agreeableness', 'extraversion']]
        w = person1.values
        w = w.T

        w_personality = {'openness': np.round(w[0].reshape(len(w[0]) ,1), 3), 'agreeableness': np.round(w[1].reshape(len(w[1]) ,1), 3), 'neuroticism':np.round(w[2].reshape(len(w[2]) ,1), 3), 'conscientiousness': np.round(w[3].reshape(len(w[3]) ,1), 3), 'extraversion': np.round(w[4].reshape(len(w[4]) ,1), 3)}

        X_personality = {'openness': np.round(w_personality['openness']*x, 3), 'agreeableness':  np.round(w_personality['agreeableness']*x, 3), 'neuroticism':  np.round(w_personality['neuroticism']*x, 3), 'conscientiousness':  np.round(w_personality['conscientiousness']*x, 3), 'extraversion':  np.round(w_personality['extraversion']*x, 3)}

        learning_rate = 0.05
        num_epoch = 1000
        t_user = []

        for i in user_id_list:
            appended = False
            errors = []
            t_personality = np.full((15,1),1) # 초기 theta를 1로 초기화
            x = i[1].drop(['user_id', 'movie_id', 'movie_score'], axis=1) # x
            y = i[1]['movie_score'].values.reshape(len(i[1]),1) # y
            for epoch in range(num_epoch):
                y_hat = np.round(np.dot(x, t_personality), 2)
                loss =  y_hat - y
                cost = np.round(np.sum(np.round((loss**2) ,2) / len(x)), 2)
                gradient = np.round((np.dot(x.T, loss) / len(x)), 2)
                error = cost

                if len(errors) > 3:
                    if error < 0.1:
                        t_user.append((i[0], t_personality))
                        appended = True
                        break # error rate가 1 이하거나
            #elif error==errors[-1] and error==errors[-2] and error==errors[-3]:
               # t_user.append((i[0], t_personality))
                #break # error rate가 3번이상 똑같으면 break

                t_personality = t_personality - learning_rate * gradient
                errors.append(error)
                #if epoch % 50 == 0:
                    #print(epoch, " t = ", t, " error = ",error)

            if not appended :
                t_user.append((i[0], t_personality))
            #print("----" * 15)
            #print(i[0])
            #print(epoch ," t = ", t_personality, " error = ",error)
        #---------------- gradient end --------------------------

        Train_inputUser = train.drop('movie_id',1).drop('movie_score',1).drop_duplicates(keep='first', inplace=False)
        Test_inputUser = test.drop('movie_id',1).drop('movie_score',1).drop_duplicates(keep='first', inplace=False)
        Train_user_list = Train_inputUser.values.tolist()
        Test_user_list = Test_inputUser.values.tolist()

        Openness = []
        Neuroticism = []
        Conscientiousness = []
        Agreeableness = []
        Extraversion = []
        UserDistance = []

        for i in range(0,len(Test_user_list)):
            Distance = []
            for j in range(0,len(Train_user_list)):
                openness = (Test_user_list[i][1] - Train_user_list[j][1])
                neuroticism = (Test_user_list[i][2] - Train_user_list[j][2])
                conscientiousness = (Test_user_list[i][3] - Train_user_list[j][3])
                agreeableness = (Test_user_list[i][4] - Train_user_list[j][4])
                extraversion = (Test_user_list[i][5] - Train_user_list[j][5])

                Openness = openness**2
                Neuroticism = neuroticism**2
                Conscientiousness = conscientiousness**2
                Agreeableness = agreeableness**2
                Extraversion = extraversion**2

                Distance.append(Openness + Neuroticism + Conscientiousness + Agreeableness + Extraversion)

            for k in range(0,len(Distance)):
                if(Distance[k] == min(Distance)):
                    num_min = k
            UserDistance.append(num_min)

        #----------------- distance end ----------------
        ERROR = []
        for i in range(0, len(UserDistance)):
            for j in range(0, len(t_user)):
                if Train_user_list[UserDistance[i]][0] == t_user[j][0]:
                    x_test = user_id_list[j][1].drop(['user_id', 'movie_id', 'movie_score'], axis=1)
                    real_rating = user_id_list[j][1]['movie_score'].values.reshape(len(user_id_list[j][1]['movie_score'].values),1)
                    prediction_rating = np.dot(x_test, t_user[j][1])
                    error = abs(prediction_rating - real_rating) / real_rating * 100
            #print(error)
            #print(t_user[j][0])
            #print(sum(error)/len(error))
            ERROR.append(sum(error)/len(error))
        # ------------------- get error end -------------

        print(100 - sum(ERROR)/len(ERROR))
```

    [72.49211598]
    [71.02369153]
    [71.66888455]
    [71.91870593]
    [71.27142096]

```python

```


# RMP3 与 RMP4 能量

> 创建时间：2019-11-01

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

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

这一篇文档的主要参考是 Helgaker et al. 2013 教材 [^Helgaker-Jorgensen.Wiley.2013] 的 section 14.4。关于 Spin-Orbital MP3 与 RMP3 的另一种实现方式，可以参考以 Szabo [^Szabo-Ostlund.Dover.1996] 公式为蓝本的 Psi4NumPy 简要代码 [^psi4numpy-mp3]。

```python
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)
```

## 分子体系与标准结果

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

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

- `C`, `e` 分别表示分子轨道系数 $C_{\mu p}$ 与轨道能 $\varepsilon_p$

```python
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)
```

```python
scf_eng = scf.RHF(mol)
scf_eng.conv_tol = 1e-12
scf_eng.max_cycle = 128
scf_eng.kernel()
```

    -150.5850337808384

```python
C, e = scf_eng.mo_coeff, scf_eng.mo_energy
Co, Cv = C[:, so], C[:, sv]
eo, ev = e[so], e[sv]
```

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

```python
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-")
```

其中一些结论有：

```python
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

## 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{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}
$$

```python
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}
$$

```python
energy_mp2_corr = (t_iajb * L_mo[so, sv, so, sv]).sum()
energy_mp2_corr
```

    -0.26901177170795454

## 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}
$$

```python
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}
$$

```python
energy_mp3_corr = (T_iajb * mp3_X_iajb).sum()
energy_mp3_corr
```

    0.007451743819516383

## RMP4(SDQ) 相关能

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

### 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) |

```python
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}
$$

```python
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}
$$

```python
energy_mp4_S = (T_iajb * mp4_S_iajb).sum()
energy_mp4_S
```

    -0.004382160888639593

### 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` 转置来执行对称化。

```python
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}
$$

```python
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}
$$

```python
energy_mp4_D = (T_iajb * mp4_D_iajb).sum()
energy_mp4_D
```

    -0.008819028049601586

### RMP4(Q) 相关能

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

$$
\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}
$$

```python
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}
$$

```python
energy_mp4_Q = (T_iajb * mp4_Q_iajb).sum()
energy_mp4_Q
```

    0.001135577514293328

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

```python
energy_mp4SDQ_corr = energy_mp4_S + energy_mp4_D + energy_mp4_Q
energy_mp4SDQ_corr
```

    -0.01206561142394785

## RMP4(T) 相关能

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

### 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}
$$

```python
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}
$$

```python
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}
$$

```python
energy_mp4_T = (T_iajb * mp4_T_iajb).sum()
energy_mp4_T
```

    -0.006712603570763939

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

```python
energy_mp4_corr = energy_mp4_S + energy_mp4_D + energy_mp4_T + energy_mp4_Q
energy_mp4_corr
```

    -0.018778214994711787

### 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}$ 在这里可能不适用；因此需要手输所有的中间张量的缩并过程。下述函数可以用来辅助我们进行带有指标转换的张量缩并。

```python
# 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)$，那么我们执行下述代码就可以生成转换后的缩并字符串了：

```python
substitute_string("iajd, ckbd", "iajbkc -> jbkcia")
```

    'jbkd, aicd'

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

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

```python
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

[^Helgaker-Jorgensen.Wiley.2013]: Helgaker, T.; Olsen, J.; Jorgensen, P. *Molecular Electronic-Structure Theory*; Wiley-Blackwell, 2013-02-01.

[^Szabo-Ostlund.Dover.1996]: Szabo, A.; Ostlund, N. S. *Modern Quantum Chemistry: Introduction to Advanced Electronic Structure Theory (Dover Books on Chemistry)*; Dover Publications, 1996.

[^psi4numpy-mp3]: <https://github.com/psi4/psi4numpy/tree/master/Moller-Plesset>