# 频率分析 (4)：非谐性频率矫正 VPT2

在本文档中，我们将会讨论非谐性频率矫正模型 VPT2。

在量化计算中，通常分子振动的频率通过 Hessian 矩阵给出。但我们也知道，Hessian 矩阵是能量对原子核坐标的两次导数；以这种方式求得的频率，相当于求取抛物线势函数的频率。但真实的势函数并非抛物线 (譬如 Morse、L-J 势函数等)，因此真实的频率会与 Hessian 矩阵所得到的值有少许区别。这也会影响到热力学矫正中，频率配分函数所产生的贡献值。

为了获得更真实的分子振动信息，那么就需要求解 Hamiltonian 受分子振动微扰后的本征波函数，并进而进行分析。近似的做法可以是微扰理论的矫正。这篇文档是其中一种近似方法，称为 Vibrational Perturbation Theory to the Second-Order (VPT2)。

我们主要会依据 V. Barone [^Barone.JCP.2005] 所提供的算法与公式进行实现。我们不对公式与其正确性作推演与检查。我们主要与 Gaussian 16 rev. B01 的结果作参照，但不会完全相同。

:::{note}

在 4 Core CPU 上完整执行本文档需要大约 5-10 分钟。

:::

::: {warning}

我们在本文档中使用处于非平衡构型的非对称的氨分子。

- **频率分析没有物理意义**：频率分析原则上一定要处于平衡构型；特殊的情况会是过渡态。除此之外的情况，频率分析都没有物理意义。需要先优化到稳定的构型再进行分析。
- **不考察对称性**：对于对称的分子，有可能会产生简并的频率。在一些情况下，简并会产生数值上的奇点。非简并的分子不太容易产生奇点，分析相对容易。
- **非线性分子**：很多时候，线性与非线性分子需要分别使用不同的方法分析。我们只考虑非线性分子，即 $3N-6$ 型分子。

:::

:::{danger}

若本文档有幸 (或不幸) 能帮助到读者，读者也需要知道**这份文档的程序实现未必正确！**

本文档所用于计算的体系比较特别 (有虚频的氨、能量最低结构的乙醛)，**无法保证可以应用到其它的分子**。

同时，**Gaussian 的不同版本有不同的 bug 和使用技巧** (当然完全可能是我的错误，但这应由读者去判断与负责)。我们会在具体的位置，给出各个版本存在潜在问题的情况；并且可能的话，会给出如何重复出 Gaussian 的结果。对于输入卡 {download}`anharm.gjf` 我们所考察的 Gaussian 版本有 16 rev. B01 {download}`anharm-G16B01.log`, 09 rev. D01 {download}`anharm-G09D01.log`, 09 rev. C01 {download}`anharm-G09C01.log`。

读者特别需要注意在作性质计算时，**大多数时候我们采用 Eckart Orientation**。

由于当前的 VPT2 理论本身就不适合用于小分子以外的计算 (容易由于频率共振而给出非常离谱的非谐矫正值)，因此即使看起来这里给出的做法似乎适合大分子，并且程序也看起来不会报错，但**必须要慎用当前文档所实现的 VPT2**。VPT2 有不止一种应用方式；Gaussian 也实现了一些变种，可以一定程度上解决共振效应，这是本文档我完全无法处理的。

:::

VPT2 分析会稍微复杂一些。我们在这里尽管会与 Gaussian 16 的结果作对照，但所有量化计算都会在 PySCF 进行。为此我们需要对 Hessian 矩阵作基础的 [频率分析](freq_1.ipynb)，并且需要进行 [数值导数](https://py-xdh.readthedocs.io/zh_CN/latest/numdiff/nuc_grad.html)；这两个功能分别实现在 {download}`freqanal.py` 与 {download}`deriv_numerical.py` 中。

```python
%matplotlib notebook

from freqanal import FreqAnal
from deriv_numerical import NumericDiff, NucCoordDerivGenerator

import itertools
import numpy as np
from matplotlib import pyplot as plt
from opt_einsum import contract as einsum
from pyscf import gto, scf, hessian, lib, data

np.set_printoptions(6, linewidth=150, suppress=True)
```

我们也需要进行单位换算。这在先前的若干文档中已经有所使用。

```python
from scipy.constants import physical_constants
# https://docs.scipy.org/doc/scipy/reference/constants.html
E_h = physical_constants["Hartree energy"][0]
a_0 = physical_constants["Bohr radius"][0]
N_A = physical_constants["Avogadro constant"][0]
c_0 = physical_constants["speed of light in vacuum"][0]
e_c = physical_constants["elementary charge"][0]
e_0 = physical_constants["electric constant"][0]
F = physical_constants["Faraday constant"][0]
k_B = physical_constants["Boltzmann constant"][0]
R = physical_constants["molar gas constant"][0]
h = physical_constants["Planck constant"][0]
hbar = physical_constants["reduced Planck constant"][0]
amu = physical_constants["atomic mass constant"][0]
```

除了文末的红外光谱绘制，我们使用的分子是不具有对称性的氨分子，基组 STO-3G。

```python
mol = gto.Mole(atom="""
N  0.000000    0.000000    0.000000
H  0.000000    0.000000    0.940000
H  1.006874    0.000000   -0.260395
H -1.037114   -0.277894   -0.640054
""", basis="STO-3G", verbose=0).build()
```

## 数值三阶与四阶核坐标导数

### 二阶导数回顾与频率数值

:::{admonition} 记号定义

**角标定义**

- $A, B$：原子核标号；
- $\alpha, \beta, \gamma$：三维坐标的取向 (即可以取 $x, y, z$，也可能取转动惯量主轴，本文档多数时间选择后者)；
- $i, j, k$：简振模式的编号。

**物理量定义**

- $A_\alpha$：原子核 $A$ 的坐标三维分量 $\alpha$；
- $Q_i$：一单位的简振模式 $i$；
- $Q_{A_\alpha, i}$：简振模式转换矩阵，即 $i$ 在原子坐标分量 $A_\alpha$ 的分量值。

需要注意到，这些记号的定义与量化程序中的定义，也与之前文档的定义是冲突或不同的。读者需要注意记号的差别。

:::

我们知道，Hessian 矩阵是频率分析的必要矩阵，它即是二阶核坐标导数，及

$$
\Phi_{A_\alpha B_\beta} = \frac{\partial^2 E}{\partial A_t \partial B_s}
$$

在原子坐标分量表示下，这应当是一个 $(A_\alpha, B_\beta): (3 n_\mathrm{atm}, 3 n_\mathrm{atm})$ 的矩阵。尽管依据程序不同，也可能是张量——在 PySCF 中，Hessian 的默认输出是 $(A, B, \alpha, \beta): (n_\mathrm{atm}, n_\mathrm{atm}, 3, 3)$ 维度的张量，但张量的大小与 $(A_\alpha, B_\beta)$ 实际上一致，经过转置两者就能相同。

```python
natm = mol.natm
nhess = natm * 3

mf = scf.RHF(mol).run()
mf_hess = mf.Hessian().run()
```

我们也需要定义频率分析的实例 `fa`，其变量名是 `FreqAnal` 的首字母，与♂fa 乐器♂无关。下面的 code cell 输出的是以 $\mathrm{cm^{-1}}$ 为简振振动频率 $\omega_i$。

如果要与 Gaussian 的结果有更好的印证，原子质量需要取与 Gaussian 相同的值，而不太适合用整数值。因此，这里没有用 PySCF 自带的 `mol.atom_mass_list`，而是使用一个函数 `get_atom_mass_list` 来解决问题。

```python
def get_atom_mass_list(mol):
    d = {1: 1.00782504, 6: 12., 7: 14.0030740, 8: 15.9949146}
    return np.array([d[m] for m in mol.atom_charges()])
```

```python
fa = FreqAnal()
fa.mol_weights = get_atom_mass_list(mol)
fa.mol_coords = mol.atom_coords()
fa.natm = mol.natm
fa.mol_hess = mf_hess.de.swapaxes(1, 2)
nvib = fa.freq.size
fa.freq
```

    array([-969.746082, 1680.3876  , 1931.786797, 2059.643873, 3874.822068, 5095.777567])

:::{admonition} 记号定义

我们之后会经常作单位换算与量纲分析，因此这里尽量将目前与将来需要的物理量与量纲列出：

- $Q_i$ 简振模式，单位 $\mathrm{Bohr \ amu^{1/2}}$；
- $Q_{A_\alpha i}$ 简振模量到分子坐标表象的转换矩阵，单位 $\mathrm{amu^{-1/2}}$，程序调用 `fa.q`；
- $Q_{A_\alpha i}^\mathrm{norm}$ 归一化的简振模量转换矩阵，无量纲，程序调用 `fa.qnorm`；
- $\omega_i$ 简振振动频率，单位 $\mathrm{cm^{-1}}$，程序调用 `fa.freq`；
- $\nu_i$ 非谐振动频率，单位 $\mathrm{cm^{-1}}$；
- $\lambda_i$ 二阶力常数，单位 $\mathrm{\mathit{E}_h \ Bohr^{-2} \ amu^{-1}}$；
- $\Phi_{A_\alpha B_\beta}$ 坐标表示的 Hessian 矩阵，单位 $\mathrm{\mathit{E}_h \ Bohr^{-2}}$；
- $\Phi_{ij}$ 简振表示的 Hessian 矩阵，单位 $\mathrm{\mathit{E}_h \ Bohr^{-2} \ amu^{-1}}$；
- $\Phi_{ijk}$ 简振表示的三阶导数，单位 $\mathrm{\mathit{E}_h \ Bohr^{-3} \ amu^{-3/2}}$；
- $\Phi_{ijkl}$ 简振表示的四阶导数，单位 $\mathrm{\mathit{E}_h \ Bohr^{-4} \ amu^{-2}}$；
- $I_\alpha$ 转动惯量，单位 $\mathrm{amu \ Bohr^{2}}$；
- $\varpi_\alpha$ 转动常数，单位 $\mathrm{cm^{-1}}$；
- $\zeta_{ij}^\alpha$ Coriolis 耦合常数，无量纲；
- $\theta_i (T)$ 温度矫正系数，无量纲；
- $\boldsymbol{\mu}$ 偶极矩，单位 $\mathrm{Debye}$；

:::

为了后文的便利，我们也会经常使用在简振模式表示下的 Hessian 矩阵：

$$
\Phi_{ij} = \frac{\partial^2 E}{\partial Q_i \partial Q_j}
$$

从定义上，简振模式是分子振动的 Hessian 矩阵的本征模式，因此 $\Phi_{ij}$ 实际上是对角矩阵 $\lambda_i \delta_{ij}$。其中，$\lambda_i$ 是简振模式 $i$ 的振动强度。坐标表示与简振表示的换算关系是

$$
\Phi_{ij} = \sum_{A_\alpha B_\beta} Q_{A_\alpha i} \Phi_{A_\alpha B_\beta} Q_{B_\beta j}
$$

我们用 `deriv_2` 表示 $\Phi_{ij}$。

```python
deriv_2 = einsum("Pi, PQ, Qj -> ij", fa.q, fa.mol_hess.reshape(nhess, nhess), fa.q)
deriv_2
```

    array([[-0.035588, -0.      ,  0.      ,  0.      , -0.      , -0.      ],
           [-0.      ,  0.106859,  0.      , -0.      ,  0.      ,  0.      ],
           [ 0.      ,  0.      ,  0.141224, -0.      ,  0.      ,  0.      ],
           [ 0.      , -0.      , -0.      ,  0.160537, -0.      , -0.      ],
           [ 0.      ,  0.      , -0.      , -0.      ,  0.568192, -0.      ],
           [-0.      , -0.      , -0.      , -0.      , -0.      ,  0.982681]])

我们注意到这是一个对称矩阵。实际上，简振模式从定义上就是在 Hessian 矩阵去除了平动、转动贡献之后的本征振动模式；因此，在简振模式表示下的 Hessian 矩阵自然应当是对角化的。其对角元我们之后也经常用到，因此以 `lambd` $\lambda_i = \Phi_{ij} \delta_{ij}$ 表示，物理量上称为力常数。

```python
lambd = deriv_2.diagonal()
lambd
```

    array([-0.035588,  0.106859,  0.141224,  0.160537,  0.568192,  0.982681])

力常数 $\lambda_i$ 它与频率 $\omega_i$ 之间的关系是

$$
\omega_i = \frac{\sqrt{\lambda_i}}{2 \pi c_0} = \frac{1}{2 \pi c_0} \sqrt{\frac{k_i}{m}}, \quad \lambda = (2 \pi c_0 \omega_i)^2
$$

它与我们经常看到的另一个力常数 $k_i$ 不同。从量纲上，$\lambda_i$ 是 $\mathrm{[T]^{-2}}$。如果考虑到一些比较麻烦的单位换算，我们可以通过频率 $\omega_i$ 给出力常数 $\lambda_i$。

单位换算的原则如下。我们发现，对于等式左边的待求量完全是原子单位，但右边则完全是经过变动的 SI 单位制 (波数以 $\mathrm{cm^{-1}}$ 而非以 $\mathrm{m^{-1}}$ 表示)。我们以 SI 单位制作为中间单位，
- 对等式左边，$\lambda_i$ 的单位是 $\mathrm{\mathit{E}_h \ Bohr^{-2} \ amu^{-1}}$，我们要对应地除以这部分单位的 SI 转换；
- 对等式右边，$\omega_i$ 的单位是 $\mathrm{cm^{-1}}$，我们要乘以这部分的 SI 转换 ($100$)。

```python
(
    (2 * np.pi * c_0 * fa.freq)**2        # equation
    / (E_h * a_0**-2 * amu**-1) * 100**2  # unit conversion
)
```

    array([0.035588, 0.106859, 0.141224, 0.160537, 0.568192, 0.982681])

我们最后讨论一下简振模量矩阵 $Q_{A_\alpha i}$ 与其归一化形式 $Q_{A_\alpha i}^\mathrm{norm}$ 之间的关系。在 Gaussian 的输出中，我们一般只会看到 $Q_{A_\alpha i}^\mathrm{norm}$，它具有下述限制：

$$
\sum_{A_\alpha} (Q_{A_\alpha i}^\mathrm{norm})^2 = 1 , \ \forall i
$$

正因为这种归一化条件，$Q_{A_\alpha i}^\mathrm{norm}$ 实际上是没有量纲的。但我们以后一直要遇到与处理的简振模量，是所谓分子质量折合过的模量，其量纲是 $\mathrm{[M]^{-1/2}}$。它也可以从归一化的简振模量导出，其中 $m_\mathrm{A}$ 是 $A$ 原子的质量：

$$
Q_{A_\alpha i} = Q_{A_\alpha i}^\mathrm{norm} \left( \sum_{A_\alpha} \big( Q_{A_\alpha i}^\mathrm{norm} \sqrt{m_\mathrm{A}} \big)^2 \right)^{-1/2}
$$

```python
np.allclose(fa.qnorm / np.linalg.norm(fa.qnorm * np.sqrt(np.repeat(get_atom_mass_list(mol), 3))[:, None], axis=0), fa.q)
```

    True

### 数值三阶导数

我们以后一直会使用简振坐标表示下的能量导数；并且我们注意到简振模式的数量是 $3N-6$，比原子坐标分量的 $3N$ 数量要少一些。因此，在求取数值导数时，我们会希望使用简振模式作为实际被求导量。

我们需要的三阶导数量是

$$
\Phi_{ijk} = \frac{\partial^3 E}{\partial Q_i \partial Q_j \partial Q_k}
$$

但在求取数值导数时，我们可以选择一个量作为数值偏移量，譬如 $Q_k$；而剩下的量则是解析地计算。但我们也注意到，程序上默认给出的是原子坐标表示的 $\Phi_{A_\alpha B_\beta}$，还需要我们作转换得到 $\Phi_{ij}$。因此，数值导数的求取方法是

$$
\Phi_{ijk} = \sum_{A_\alpha B_\beta} Q_{A_\alpha i} Q_{B_\beta j} \Phi_{A_\alpha B_\beta k} = \sum_{A_\alpha B_\beta} Q_{A_\alpha i} Q_{B_\beta j} \frac{\Phi_{A_\alpha B_\beta} (\delta Q_k) - \Phi_{A_\alpha B_\beta} (-\delta Q_k)}{2 \delta Q_k}
$$

下面的程序就是以 $0.01 \ \mathrm{\mathring{A}} \ \mathrm{amu}^{1/2}$ 为偏移大小，求取对简振坐标导数的程序：

```python
class ModeDerivGenerator(NucCoordDerivGenerator):

    def __init__(self, mol, mf_func, q, interval=3e-3):
        self.q = q
        super(ModeDerivGenerator, self).__init__(mol, mf_func, stencil=3, interval=interval)

    def init_objects(self):
        nmode = self.q.shape[1]
        self.objects = np.empty((nmode, 2), dtype=object)

    def perform_mf(self):
        natm = self.mol.natm
        nmode = self.q.shape[1]
        for nq, norm in enumerate(self.q.T):
            norm = norm.reshape(natm, 3)
            movelist = [(A, t, norm[A, t]) for A in range(natm) for t in range(3)]
            self.objects[nq, 1] = self.mf_func(self.move_mol(movelist))
            movelist = [(A, t, - norm[A, t]) for A in range(natm) for t in range(3)]
            self.objects[nq, 0] = self.mf_func(self.move_mol(movelist))
```

我们将 Hessian 对简振坐标的导数实例定为 `num_hess`：

```python
num_hess = ModeDerivGenerator(mol, lambda mol: scf.RHF(mol).run().Hessian().run(), fa.q, interval=0.01)
```

使用 `NumericDiff` 直接求导的结果是 $\Phi_{A_\alpha B_\beta k}$。我们需要再对其进行转换再得到 $\Phi_{ijk}$ `deriv_3`：

```python
tmp_3 = NumericDiff(num_hess, lambda mf: mf.de.swapaxes(1, 2).reshape(nhess, nhess)).derivative
deriv_3 = einsum("Ai, Bj, kAB -> ijk", fa.q, fa.q, tmp_3)
```

严格地来说，$\Phi_{ijk}$ 应当是具有六重对称性的张量。因此，我们还可以对其进行对称化，使得结果定性上更正确：

```python
deriv_3 = 1/3 * (deriv_3 + deriv_3.transpose(1, 2, 0) + deriv_3.transpose(2, 0, 1))
```

### 数值四阶导数

我们通过三点差分的方法，给出了三阶导数。尽管我们无法求出全部的四阶导数，但利用刚才求取三阶导数的数据，我们可以求取 $\Phi_{ijkk}$ 形式的四阶导数：

$$
\Phi_{ijkk} = \frac{\partial^2}{\partial Q_k^2} \frac{\partial^2 E}{\partial Q_i \partial Q_j} = \frac{\Phi_{ij} (\delta Q_k) - 2 \Phi_{ij} (0) + \Phi_{ij} (-\delta Q_k)}{(\delta Q_k)^2}
$$

注意到利用现有的数据，我们只能求取 $\Phi_{ijkk}$，而不能求取更一般的 $\Phi_{ijkl}$。该张量也具有对称性；我们可以对 $i, j$ 角标进行对称化。我们将会用 `deriv_4` 表示 $\Phi_{ijkk}$，维度是 $(i, j, k)$

```python
hess_gathered = np.array([[mf.de.swapaxes(1, 2).reshape(nhess, nhess) for mf in l] for l in num_hess.objects])
tmp_4 = einsum("ksAB, Ai, Bj -> skij", hess_gathered, fa.q, fa.q)
deriv_4 = (tmp_4[0] + tmp_4[1] - 2 * deriv_2) / (num_hess.interval**2)  # finite diff
deriv_4 = (deriv_4 + deriv_4.swapaxes(-1, -2)) / 2                      # symmetrize
deriv_4 = einsum("kij -> ijk", deriv_4)                                 # subscript transform
```

## 非谐频率矫正

在求取频率矫正前，我们需要先获得矩阵 $\xi_{ij}$ `xmat`。该矩阵与频率相同单位，即 $\mathrm{cm}^{-1}$。这里的公式主要参考 Barone eq. (36-38)，但在与一些系数有关的问题上不太相同。该矩阵对非谐矫正后的频率至关重要。

### 四阶导数对 $\xi_{ij}$ 的贡献

若除开单位换算，四阶导数对矩阵 $\xi_{ij}$ 的贡献是

$$
\begin{align}
\xi_{ii} &\leftarrow \frac{\hbar}{2 \pi c_0} \frac{1}{16 \lambda_i} \Phi_{iiii} \\
\xi_{ij} &\leftarrow \frac{\hbar}{2 \pi c_0} \frac{1}{4 \sqrt{|\lambda_i \lambda_j|}} \Phi_{iijj} \quad (i \neq j)
\end{align}
$$

之所以表达式中需要引入绝对值，是因为我们同时处理实频与虚频。虚频下，$\lambda_i$ 小于零，无法开根号。依据 Miller, et al. [^Miller.CPL.1990] 的表达式，实际上对于有且仅有一个虚频的 $Q_i$，其对应的 $\xi_{ij}$ 也应当是虚数。当前的分子也恰好是有且仅有一个虚频，并且实际上虚数的 $\xi_{ij}$ 可以与虚数的 $\omega_i$ 相加得到非谐频率 $\nu_i$；因此就这种过渡态情形，在程序实现上，可以对虚频与实频一视同仁。

```python
xmat_4 = np.zeros((nvib, nvib))
for i in range(nvib):
    xmat_4[i, i] = deriv_4[i,i,i] / (16 * lambd[i])
    for j in range(nvib):
        if i == j: continue
        xmat_4[i, j] = deriv_4[i,i,j] / (4 * np.sqrt(np.abs(lambd[i] * lambd[j])))
xmat_4 *= (hbar / (2 * np.pi * c_0)) * (a_0**-2 * amu**-1) / (100)
xmat_4
```

    array([[ -59.929105,   17.359227,    2.022484,  -26.747814,  -89.778445, -133.911891],
           [  17.360842,   11.4939  ,   18.955104,   -6.407765,   -6.83072 , -112.008037],
           [   2.024055,   18.952477,    4.4002  ,   42.205352,  -50.906768,  -38.849444],
           [ -26.737152,   -6.40661 ,   42.197109,   45.874797,  -16.574222,   -5.321896],
           [ -89.725547,   -6.828563,  -50.876096,  -16.565645,   62.64625 ,    1.480829],
           [-133.825551, -111.932215,  -38.826908,   -5.319965,    1.480938,   70.703952]])

### 三阶导数对 $\xi_{ij}$ 的贡献

三阶导数对 $\xi_{ij}$ 的贡献核算相对复杂。我们首先处理对角线的贡献：

$$
\xi_{ii} \leftarrow - \frac{\hbar}{2 \pi c_0} \frac{1}{16 \lambda_i} \left( \frac{5}{3 \lambda_i} \Phi_{iii}^2 + \frac{8 \lambda_i - 3 \lambda_j}{\lambda_j (4 \lambda_i - \lambda_j)} \Phi_{iij}^2 \right)
$$

```python
xmat_3 = np.zeros((nvib, nvib))
```

```python
for i in range(nvib):
    xmat_3[i, i] -= deriv_3[i,i,i]**2 * 5 / (3 * lambd[i])
    for j in range(nvib):
        if i == j: continue
        xmat_3[i, i] -= deriv_3[i,i,j]**2 * (8 * lambd[i] - 3 * lambd[j]) / (lambd[j] * (4 * lambd[i] - lambd[j]))
    xmat_3[i, i] /= 16 * lambd[i]
```

随后处理非对角线的贡献 ($i \neq j$)：

$$
\begin{align}
\xi_{ij} &\leftarrow - \frac{\hbar}{2 \pi c_0} \frac{1}{4 \sqrt{|\lambda_i \lambda_j|}} \left( \frac{2}{4 \lambda_i - \lambda_j} \Phi_{iij}^2 + \frac{2}{4 \lambda_j - \lambda_i} \Phi_{ijj}^2 + \frac{\Phi_{iii} \Phi_{ijj}}{\lambda_i} + \frac{\Phi_{jjj} \Phi_{iij}}{\lambda_j} \right) \\
&\quad + \frac{\hbar}{2 \pi c_0} \frac{1}{4 \sqrt{|\lambda_i \lambda_j|}} \sum_{k \notin \{i, j\}} \left[ \frac{2 (\lambda_i + \lambda_j - \lambda_k)}{\Delta_{ijk}} \Phi_{ijk}^2 - \frac{\Phi_{iik} \Phi_{jjk}}{\lambda_k} \right]
\end{align}
$$

```python
for i in range(nvib):
    for j in range(nvib):
        if i == j: continue
        xmat_3[i, j] -= deriv_3[i,i,j]**2 * 2 / (4 * lambd[i] - lambd[j])
        xmat_3[i, j] -= deriv_3[i,j,j]**2 * 2 / (4 * lambd[j] - lambd[i])
        xmat_3[i, j] -= deriv_3[i,i,i] * deriv_3[i,j,j] / lambd[i]
        xmat_3[i, j] -= deriv_3[j,j,j] * deriv_3[i,i,j] / lambd[j]
        for k in range(nvib):
            if len(set([i, j, k])) != 3: continue
            delta_ijk = lambd[i]**2 + lambd[j]**2 + lambd[k]**2 - 2 * (lambd[i]*lambd[j] + lambd[j]*lambd[k] + lambd[k]*lambd[i])
            xmat_3[i, j] += deriv_3[i,j,k]**2 * 2 * (lambd[i] + lambd[j] - lambd[k]) / delta_ijk
            xmat_3[i, j] -= deriv_3[i,i,k] * deriv_3[j,j,k] / lambd[k]
        xmat_3[i, j] /= 4 * np.sqrt(np.abs(lambd[i] * lambd[j]))
```

```python
xmat_3 *= (hbar / (2 * np.pi * c_0)) * (a_0**-2 * amu**-1) / (100)
xmat_3
```

    array([[   6.436059,  -17.659895,  -12.843587,  -17.730696,   49.859864,   86.717056],
           [ -17.659895,  -18.294258,  -35.959998,  -46.395273,    2.625465,   94.139235],
           [ -12.843587,  -35.959998,  -65.430162, -108.220206,  228.295265,   26.274758],
           [ -17.730696,  -46.395273, -108.220206, -115.728507,  -19.13443 ,   -3.494852],
           [  49.859864,    2.625465,  228.295265,  -19.13443 , -111.055075,  -10.265471],
           [  86.717056,   94.139235,   26.274758,   -3.494852,  -10.265471, -112.711441]])

### Coriolis 矫正对 $\xi_{ij}$ 的贡献

Coriolis 矫正的贡献方式是 (允许 $i = j$)

$$
\xi_{ij} \leftarrow \sum_\alpha \frac{\lambda_i + \lambda_j}{\sqrt{|\lambda_i \lambda_j|}} \varpi_\alpha (\zeta_{ij}^\alpha)^2
$$

这里引入了一些特殊的记号。被求和的角标 $\alpha$ 尽管是 $x, y, z$ 三个取向之一，但在求取 Coriolis 矫正贡献时，它必须是转动惯量主轴。

我们在通过 Hessian 矩阵得到分子振动频率时，需要去除转动的三个自由度贡献。那时我们已经计算过与转动有关的各种中间变量。$\varpi_\alpha$ `rot_wavenum` 是以 $\mathrm{cm^{-1}}$ 为单位的转动常数：

$$
\varpi_\alpha = \frac{h}{8 \pi^2 c_0 I_\alpha}
$$

```python
rot_wavenum = h / (8 * np.pi**2 * c_0 * fa.rot_eig) * (a_0**-2 * amu**-1) / (100)
rot_wavenum
```

    array([13.875725,  7.153573,  4.775983])

而 $\zeta_{ij}^\alpha$ `zeta` Coriolis 耦合常数通过下式进行计算 (Barone, eq (11)，维度 $(\alpha, i, j)$，无量纲)：

$$
\begin{align}
\zeta_{ij}^\alpha &= \sum_k (L_{A_\beta i} L_{A_\gamma j} - L_{A_\gamma i} L_{A_\beta j}) \\
L_{A_\alpha i} &= \sum_\beta Q_{A_\beta i} R_{\beta \alpha} \sqrt{m_A}
\end{align}
$$

其中，$R_{\beta \alpha}$ 转动的本征向量，无量纲。其本征值是转动惯量，这在 [频率分析 (1)](freq_1.ipynb) 中有所提及。同时，求取 $\zeta_{ij}^\alpha$ 时，其表达式的分量 $\alpha, \beta, \gamma$ 必须按照右手坐标系排序。

```python
q_ = einsum("Aβi, βα, A -> Aαi", fa.q.reshape(natm, 3, nvib), fa.rot_vec, np.sqrt(get_atom_mass_list(mol)))
zeta_x = einsum("Ai, Aj -> ij", q_[:, 1, :], q_[:, 2, :]) - einsum("Ai, Aj -> ij", q_[:, 2, :], q_[:, 1, :])
zeta_y = einsum("Ai, Aj -> ij", q_[:, 2, :], q_[:, 0, :]) - einsum("Ai, Aj -> ij", q_[:, 0, :], q_[:, 2, :])
zeta_z = einsum("Ai, Aj -> ij", q_[:, 0, :], q_[:, 1, :]) - einsum("Ai, Aj -> ij", q_[:, 1, :], q_[:, 0, :])
zeta = np.array([zeta_x, zeta_y, zeta_z])
```

同时，转动本征向量还可以用来将分子旋转到转动主轴坐标系，且该坐标系的 $I_x < I_y < I_z$。该坐标系在 Gaussian 中称为 Eckart Orientation。我们可以输出该坐标系下以 Angstrom 为单位的分子构型。

```python
fa.centered_coord @ fa.rot_vec * a_0 * 1e10
```

    array([[ 0.003095, -0.002241, -0.016268],
           [ 0.384988,  0.851931,  0.073996],
           [ 0.805212, -0.657419,  0.078348],
           [-1.233207, -0.163374,  0.07369 ]])

至此，我们就可以将 Coriolis 矫正的贡献计算出来。

$$
\xi_{ij} \leftarrow \sum_\alpha \frac{\lambda_i + \lambda_j}{\sqrt{|\lambda_i \lambda_j|}} \varpi_\alpha (\zeta_{ij}^\alpha)^2
$$

```python
xmat_coriol = np.zeros((nvib, nvib))
for i in range(nvib):
    for j in range(nvib):
        for ixyz in range(3):
            xmat_coriol[i, j] += zeta[ixyz, i, j]**2 * rot_wavenum[ixyz]
        xmat_coriol[i, j] *= (lambd[i] + lambd[j]) / np.sqrt(np.abs(lambd[i] * lambd[j]))
xmat_coriol
```

    array([[-0.      ,  6.485503,  2.854787,  7.462297, 14.215879, 25.469408],
           [ 6.485503,  0.      ,  0.862218,  2.938909,  0.906227,  8.749904],
           [ 2.854787,  0.862218,  0.      ,  0.883756,  5.835533,  3.491808],
           [ 7.462297,  2.938909,  0.883756,  0.      ,  1.970819,  0.474133],
           [14.215879,  0.906227,  5.835533,  1.970819,  0.      ,  0.009302],
           [25.469408,  8.749904,  3.491808,  0.474133,  0.009302,  0.      ]])

:::{danger}

对于 Gaussian 09 rev. D01 及 C01，其输出的 Coriolis 贡献是错误的。其原因在于没有将分子坐标预先转置到 Eckart Orientation 下，而仍然在输入坐标下进行计算。将这种坐标与转动惯量主轴的转动频率作乘积没有意义。也因此，该版本的 Gaussian 实际上无法正确地输出频率的矫正；除非输入坐标与 Eckart Orientation 一致。

如果要重复该版本的结果，**对于当前的分子**，可以使用后面一个被折叠的 code cell 代码。

我们之前号称正确的实现方式，与 Gaussian 16 rev. B01 的结果应当一致。

:::

```python
def xmat_coriol_G09():
    # -- What makes different --
    q_ = einsum("Aαi, A -> Aαi", fa.q.reshape(natm, 3, nvib), np.sqrt(get_atom_mass_list(mol)))
    rot_in_G09 = rot_wavenum[0], rot_wavenum[2], rot_wavenum[1]
    # -- The rest are merely the same --
    zeta = np.zeros((3, nvib, nvib))
    for α, β, γ in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]:
        zeta[α] = einsum("Ai, Aj -> ij", q_[:, β, :], q_[:, γ, :]) - einsum("Ai, Aj -> ij", q_[:, γ, :], q_[:, β, :])
    xmat_coriol = einsum("αij, α, ij -> ij", zeta**2, rot_in_G09, (lambd[:, None] + lambd[None, :]) / np.sqrt(np.abs(lambd[:, None] * lambd[None, :])))
    return xmat_coriol
xmat_coriol_G09()
```

    array([[ 0.      ,  4.90998 ,  3.505416,  9.864751, 10.909095, 26.892735],
           [ 4.90998 ,  0.      ,  0.933722,  3.050355,  0.933074,  9.206211],
           [ 3.505416,  0.933722,  0.      ,  0.902214,  5.887021,  3.409958],
           [ 9.864751,  3.050355,  0.902214,  0.      ,  1.987215,  0.43257 ],
           [10.909095,  0.933074,  5.887021,  1.987215,  0.      ,  0.009969],
           [26.892735,  9.206211,  3.409958,  0.43257 ,  0.009969,  0.      ]])

```python
! head -n 9725 anharm-G09C01.log | tail -n 10
```

     Coriolis contributions to Xmat
                    1             2             3             4             5
          1  0.000000D+00
          2  0.268927D+02  0.000000D+00
          3  0.109091D+02  0.996906D-02  0.000000D+00
          4  0.986475D+01  0.432572D+00  0.198722D+01  0.000000D+00
          5  0.350541D+01  0.340996D+01  0.588702D+01  0.902213D+00  0.000000D+00
          6  0.490997D+01  0.920621D+01  0.933076D+00  0.305035D+01  0.933724D+00
                    6
          6  0.000000D+00

### 总 $\xi_{ij}$ 矩阵与非谐频率的导出

我们首先将四阶导数、三阶导数、Coriolis 的贡献相加，得到总 $\xi_{ij}$ 矩阵。

```python
xmat = xmat_4 + xmat_3 + xmat_coriol
xmat
```

    array([[-53.493046,   6.184835,  -7.966316, -37.016213, -25.702702, -21.725427],
           [  6.18645 ,  -6.800358, -16.142676, -49.864129,  -3.299028,  -9.118898],
           [ -7.964745, -16.145302, -61.029962, -65.131098, 183.22403 ,  -9.082879],
           [-37.005551, -49.862974, -65.139341, -69.85371 , -33.737833,  -8.342616],
           [-25.649804,  -3.296871, 183.254702, -33.729256, -48.408825,  -8.77534 ],
           [-21.639087,  -9.043076,  -9.060343,  -8.340685,  -8.775231, -42.007489]])

这可以与 Gaussian 的输出作比对。需要注意，**我们的结果与 Gaussian 并不完全一致**，且不仅是由于简振模式的排序不同所产生。这是由于 Fermi 共振所致，我们后面会提及。**我们的程序无法处理 Fermi 共振**，但 Gaussian 会有一些办法。

```python
! head -n 14013 anharm-G16B01.log | tail -n 11
```

     Total Anharmonic X Matrix (in cm^-1)
     ------------------------------------
                    1             2             3             4             5
          1 -0.534804D+02
          2 -0.216582D+02 -0.420071D+02
          3 -0.256619D+02 -0.877403D+01 -0.484077D+02
          4 -0.370099D+02 -0.834034D+01 -0.337307D+02 -0.698507D+02
          5 -0.797061D+01 -0.906609D+01 -0.296660D+02 -0.651376D+02 -0.780405D+01
          6  0.617458D+01 -0.905380D+01 -0.329760D+01 -0.498632D+02 -0.161479D+02
                    6
          6 -0.680891D+01

依据 Barone eq. (43)，非谐频率很容易地通过下式给出 (单位 $\mathrm{cm^{-1}}$)：

$$
\nu_i = \omega_i + 2 \xi_{ii} + \frac{1}{2} \sum_{j \neq i} \xi_{ij} = \omega_i + \frac{3}{2} \xi_{ii} + \frac{1}{2} \sum_j \xi_{ij}
$$

```python
fa.freq + 1.5 * xmat.diagonal() + 0.5 * xmat.sum(axis=-1)
```

    array([-1119.845085,  1630.667744,  1852.176877,  1822.892296,  3833.906134,  4983.333379])

当我们与 Gaussian 的非谐频率进行比对时，会发现我们 $\mathrm{3834 \ cm^{-1}}$ 波数的结果与 Gaussian 的 $\mathrm{3748 \ cm^{-1}}$ 相差很大。

```python
! head -n 14193 anharm-G16B01.log | tail -n 9
```

     Fundamental Bands
     -----------------
         Mode(n)      Status      E(harm)  E(anharm)     Aa(x)      Ba(y)      Ca(z)
            1(1)      active      -969.747 -1119.771  14.263374   7.063101   4.568200
            2(1)      active      5095.778  4983.317  14.043335   6.985596   4.575906
     H      3(1)      active      3874.822  3747.756  14.108724   6.929780   4.566570
            4(1)      active      2059.644  1822.902  14.182793   6.771150   4.581152
            5(1)      active      1931.787  1852.185  14.252150   7.053448   4.510857
            6(1)      active      1680.388  1630.676  14.460822   7.057768   4.466441

这是源于 Gaussian 有对近简并倍频振动模式 (Fermi 共振) 的处理。在我们的问题中，$\omega_4 - 2 \omega_2 = 3834 - 2 \times 1852 = 11 (\mathrm{cm^{-1}})$ 被认为是倍频近简并的：

```python
fa.freq[4] - 2 * fa.freq[2]
```

    11.248473679877407

```python
! head -n 13948 anharm-G16B01.log | tail -n 5
```

     Fermi resonances
     ----------------

         I      J  +   K   Freq. Diff.  Red. Cubic Const.  PT2-Variat.Diff.
          3      5      5       11.248         138.473           1009.207

它对下述三阶导数贡献项 $\xi_{42} = \xi_{24}$ 会有非常大的贡献 (注意到 $\lambda_i \propto \omega_i^2$，因此下式中分母有 $\omega_4 - 2 \omega_2$ 的因式)：

$$
\xi_{24} \leftarrow - \frac{\hbar}{2 \pi c_0} \frac{1}{4 \sqrt{|\lambda_2 \lambda_4|}} \frac{2}{4 \lambda_2 - \lambda_4} \Phi_{224}^2
$$

这类表达式可以拆分为共振项与非共振项：

$$
\frac{1}{4 \lambda_i - \lambda_j} = \frac{1}{(2 \pi c_0)^2} \left( \underset{\text{non-resonant}}{\frac{1}{4 \omega_i (2 \omega_i + \omega_j)}} + \underset{\text{resonant}}{\frac{1}{4 \omega_i (2 \omega_i - \omega_j)}} \right)
$$

在 Gaussian 中，当被判断为 Fermi 共振时，就会直接排除共振项，而只保留非共振项，以避免频率矫正的奇点。同时会在非谐频率输出的部分用记号 `H` 表示这种 Fermi 共振效应。

## 简振坐标平均偏移

为了计算分子的各种性质 (譬如偶极、红外等光谱性质) 的非谐矫正矫正，我们需要给出简振坐标的平均偏移量 $\langle Q_i \rangle$ 与二阶平均偏移量 $\langle Q_i Q_j \rangle$。

我们在这一节中，为了与 Gaussian 的结果作比对，暂时不考虑转动矫正效应。

### 温度矫正效应

我们现在要考虑温度对分子振动的影响。一般来说，温度越高，分子的振动会越剧烈，其各种谱学性质与 $0 \ K$ 下的结果会偏差越大。这种温度效应是通过近似的配分函数所给出 (类似于 softmax 的效果)。虚频被认为不参与配分，因此其值置为零。该配分函数是 (Barone, eq. (59))

$$
\theta_i (T) = \left\{
\begin{alignat}{3}
& \cot \left( \frac{h \omega_i c_0}{2 k_\mathrm{B} T} \right) \quad &&\omega_i \in \mathbb{R} \\
& 0 && \omega_i \in \mathbb{I}
\end{alignat}
\right.
$$

在后续的文档中，我们始终会使用 $T_{2500} = 2500 \ \mathrm{K}$。

```python
def get_coef_temp(T, calc_imag=False):
    coef_temp = np.tanh(h * fa.freq * 100 * c_0 / (2 * k_B * T))**-1
    if not calc_imag: coef_temp[coef_temp < 0] = 0
    else: coef_temp = np.abs(coef_temp)
    return coef_temp
```

显然，在 $0 \ \mathrm{K}$ 时，实数频率的 $\theta_i (0) = 1$。在 $2500 \ \mathrm{K}$ 时，温度对简振坐标平均偏移的矫正比例 `coef_temp` 是

```python
T = 2500
coef_temp = get_coef_temp(T)
coef_temp
```

    array([0.      , 2.226801, 1.980529, 1.88035 , 1.240967, 1.1125  ])

### 一阶简振坐标平均偏移的振动贡献

我们假定虚频对谱学性质的计算不产生贡献。对于实数的简振模式 $i$，一阶的简振坐标平均偏移 $\langle Q_i \rangle^\mathrm{vib}$ 为 (Barone, eq. (56))：

$$
\langle Q_i \rangle^\mathrm{vib} (T) = - \frac{\hbar}{4 \lambda_i} \sum_j \frac{\Phi_{ijj}}{\sqrt{|\lambda_j|}} \theta_j (T)
$$

我们将常用的转换系数置于变量 `Q_scale`，并令 $\langle Q_i \rangle^\mathrm{vib} (0)$ 为 `Q_1`，$\langle Q_i \rangle^\mathrm{vib} (T_{2500})$ 为 `QT_1`。

```python
Q_scale = hbar / np.sqrt(E_h * a_0**2 * amu)
```

```python
Q_1, QT_1 = np.zeros(nvib), np.zeros(nvib)
for i in range(nvib):
    if fa.freq[i] <= 0: continue
    for j in range(nvib):
        val = - deriv_3[i,j,j] / (4 * lambd[i] * np.sqrt(np.abs(lambd[j]))) * Q_scale
        Q_1[i] += val
        QT_1[i] += val * coef_temp[j]
print("<Q> (0 K)   ", Q_1)
print("<Q> (2500 K)", QT_1)
```

    <Q> (0 K)    [ 0.        0.014627 -0.03185  -0.059824  0.01557   0.00532 ]
    <Q> (2500 K) [ 0.        0.026619 -0.055617 -0.11182   0.022404  0.005395]

Gaussian 的结果显式如下。之所以会有正负号的差异，是因为我们的简振坐标方向会与 Gaussian 有些微区别。

```python
! head -n 13347 anharm-G16B01.log | tail -n 8
```

     Average Normal Coordinates (in amu^1/2.bohr)
     --------------------------------------------
       Mode    <Q> (0)       <Q> (******)
          2     0.005318        0.005392
          3     0.015568        0.022402
          4    -0.059824       -0.111821
          5     0.031847        0.055613
          6    -0.014626       -0.026617

:::{danger}

对于这里的实现，关于如何处理虚频，Gaussian 09 rev. D01 与 Gaussian 16 rev. B01 应是一致的，但与 Gaussian 09 rev. C01 不一致。两边似乎都有不合理之处。

我们之前采用的是与 Gaussian 16 rev. B01 相似的做法。

- 对于 Gaussian 09 rev. C01，它在计算温度矫正系数时，并没有排除虚频部分。同时，该版本还只能计算室温的温度效应。使用后一个被折叠的 code cell 即可给出该版本的结果。

- 对于 Gaussian 16 rev. B01，尽管在 $2500 \ \mathrm{K}$ 时排除了虚频的贡献，但在 $0 \ \mathrm{K}$ 下仍然考虑了虚频；因此尽管说 $T_{2500}$ 是很高的温度，但 $\langle Q_i \rangle^\mathrm{vib} (0)$ 与 $\langle Q_i \rangle^\mathrm{vib} (T_{2500})$，甚至极端一些 $\langle Q_i \rangle^\mathrm{vib} (1 \ \mathrm{K})$ 的差距还是会非常大。这种做法似乎也不合理。

:::

```python
def print_Q_1_G09C01():
    coef_temp_G09C01 = np.abs(get_coef_temp(298, calc_imag=True))
    Q_1, QT_1 = np.zeros(nvib), np.zeros(nvib)
    for i in range(nvib):
        if fa.freq[i] <= 0: continue
        for j in range(nvib):
            val = - deriv_3[i,j,j] / (4 * lambd[i] * np.sqrt(np.abs(lambd[j]))) * Q_scale
            Q_1[i] += val
            QT_1[i] += val * coef_temp_G09C01[j]
    print("<Q> (0 K)   ", Q_1)
    print("<Q> (298 K) ", QT_1)
print_Q_1_G09C01()
```

    <Q> (0 K)    [ 0.        0.014627 -0.03185  -0.059824  0.01557   0.00532 ]
    <Q> (298 K)  [ 0.        0.014657 -0.031826 -0.059851  0.015479  0.005218]

### 二阶简振坐标平均偏移的振动贡献

对于实数的简振模式 $i$，一阶的简振坐标平均偏移 $\langle Q_i Q_j \rangle$ 只有在 $i = j$ 时非零。因此，我们只需要考虑 $\langle Q_i^2 \rangle (T)$ (Barone, eq. (56))：

$$
\langle Q_i^2 \rangle (T) = \frac{\hbar}{2 \sqrt{\lambda_i}}
$$

我们令 $\langle Q_i^2 \rangle (0)$ 为 `Q_2`，$\langle Q_i^2 \rangle (T_{2500})$ 为 `QT_2`。

```python
Q_2, QT_2 = np.zeros(nvib), np.zeros(nvib)
for i in range(nvib):
    if fa.freq[i] <= 0: continue
    val = 1 / (2 * np.sqrt(lambd[i])) * Q_scale
    Q_2[i] += val
    QT_2[i] += val * coef_temp[i]
print(Q_2)
print(QT_2)
```

    [0.       0.035825 0.031163 0.029228 0.015536 0.011814]
    [0.       0.079775 0.061719 0.054959 0.01928  0.013143]

Gaussian 的结果显式如下。

```python
! head -n 13338 anharm-G16B01.log | tail -n 8
```

     Mean Square Amplitudes of Normal Coordinates (in amu.bohr^2)
     -----------------------------------------------------------
     Mode    <Q^2> (0)    <Q^2> (******)    <Q^2> (******) class.
        2     0.011814       0.013143          0.318060
        3     0.015536       0.019280          0.550081
        4     0.029228       0.054959          1.946911
        5     0.031163       0.061719          2.213156
        6     0.035825       0.079775          2.924904

:::{danger}

Barone 文章的 eq. (57) 公式很有可能差了一个正负号。因此上面的实现方式与 Barone 原文并不完全一致。在 Harding et al. [^Gauss.JCP.2008] 的 eq. (3) 中，就没有出现负号；因此多半与 Gaussian 的实现是一致的。在作这部分 Double Check 后，决定在这篇文档的程序实现中去除负号。

:::

## 非谐谱学性质：偶极矩

现在我们考虑在非谐近似下，相对来说最简单的光谱性质，即偶极矩强度。

我们在获得了简振坐标平均偏移量 $\langle Q_i \rangle$ 与 $\langle Q_i^2 \rangle$，且得到平衡构型下目标性质 $P$ 对简振坐标的导数后，就可以求取特定温度非谐近似下的目标性质的值：

$$
\langle P \rangle^\mathrm{anharm} (T) \simeq P + \sum_i \frac{\partial P}{\partial Q_i} \cdot \langle Q_i \rangle (T) + \frac{1}{2} \sum_{i} \frac{\partial^2 P}{\partial Q_i^2} \cdot \langle Q_i^2 \rangle (T)
$$

尽管求取的方式并不复杂，但在获取程序输出时，仍然必须要格外当心。

### 偶极矩的高阶导数

我们知道，为获得红外光谱数据，我们需要计算偶极在原子坐标下的导数 $\partial \boldsymbol{\mu} / \partial A_\alpha$。在 Gaussian 中，只要进行频率分析 (求取能量的二阶导数)，就可以获得偶极矩的原子坐标导数。

在 PySCF 中，尽管没有特定的函数去完成偶极矩的原子坐标导数的功能；但对于开壳层的自洽场计算方法，如果我们有了 Hessian 计算的实例，那么其求取实际上只需要 15 行代码左右。下述 `get_dipderiv` 通过带入闭壳层 Hessian 计算实例，给出偶极在原子坐标下的导数 $\partial \boldsymbol{\mu} / \partial A_\alpha$ (维度：$(A, \alpha, \gamma)$，其中 $A_\alpha$ 是原子坐标，$\gamma$ 是偶极方向)。

```python
def get_dipderiv(mf_hess):
    mf, mol = mf_hess.base, mf_hess.mol
    natm, nao, nocc = mol.natm, mol.nao, mol.nelec[0]
    H_2_ao = np.zeros((3, natm, 3, nao, nao))
    int1e_irp = mol.intor("int1e_irp").reshape((3, 3, nao, nao))
    for A in range(natm):
        _, _, p0, p1 = mol.aoslice_by_atom()[A]
        H_2_ao[:, A, :, :, p0:p1] = int1e_irp[:, :, :, p0:p1]
    H_2_ao += H_2_ao.swapaxes(-1, -2)
    dipderiv_skeleton = einsum("rAtuv, uv -> rAt", H_2_ao, mf.make_rdm1())
    dipderiv_nuc = einsum("rt, A -> rAt", np.eye(3), mol.atom_charges())
    h1ao = mf_hess.make_h1(mf.mo_coeff, mf.mo_occ)
    mo1 = np.array(mf_hess.solve_mo1(mf.mo_energy, mf.mo_coeff, mf.mo_occ, h1ao)[0])
    dipderiv_U = 4 * einsum("Atui, ruv, vi -> rAt", mo1, - mol.intor("int1e_r"), mf.mo_coeff[:, :nocc])
    return (dipderiv_skeleton + dipderiv_nuc + dipderiv_U).reshape(3, 3 * natm).T
```

在可以解析地计算偶极的核坐标导数之后，仿照先前求取能量对简振模量的高阶导数，我们很容易地给出偶极矩对简振坐标的数值二阶导数、以及一部分数值三阶导数。程序计算所使用的单位是偶极使用 $\mathrm{Debye}$，简振坐标使用原子单位 $\mathrm{Bohr \ amu^{1/2}}$，所有偶极取向均变换到 Eckart Orientation。需要注意到我们无法求取完整的三阶导数，因此三阶导数量的维度与二阶相同。

- `dip_0`：$\mu_\gamma$
- `dip_1`：$\partial \mu_\gamma / \partial Q_i$，维度 $(i, \gamma)$
- `dip_2`：$\partial^2 \mu_\gamma / \partial Q_i \partial Q_j$，维度 $(i, j, \gamma)$
- `dip_3`：$\partial^3 \mu_\gamma / \partial Q_i^2 \partial Q_j$，维度 $(i, j, \gamma)$

这部分的结果可以与 Gaussian 16 rev. B01 作核验。

```python
dip_0 = mf.dip_moment() @ fa.rot_vec
dip_1 = einsum("Aα, αγ, Ai -> iγ", get_dipderiv(mf_hess), fa.rot_vec, fa.q) * data.nist.AU2DEBYE
dip_tmp_2 = NumericDiff(num_hess, lambda mf_hess: get_dipderiv(mf_hess)).derivative * data.nist.AU2DEBYE
dip_2 = einsum("iAα, αγ, Aj -> ijγ", dip_tmp_2, fa.rot_vec, fa.q)
dipderiv_gathered = np.array([[get_dipderiv(mf_hess) for mf_hess in l] for l in num_hess.objects]) * data.nist.AU2DEBYE
dip_tmp_3 = einsum("iσAα, αγ, Aj -> σijγ", dipderiv_gathered, fa.rot_vec, fa.q)
dip_3 = (dip_tmp_3[0] + dip_tmp_3[1] - 2 * dip_1) / (num_hess.interval**2)
```

    Dipole moment(X, Y, Z, Debye): -0.01359, -0.52278,  0.07319

:::{warning}

Gaussian 09 rev. D01 版本中，偶极矩的取向对应输入分子所使用的坐标系；但 Gaussian 16 rev. B01 版本中，偶极的取向对应 Eckart Orientation。本文档使用后者；读者需要小心地处理坐标系取向。

:::

### 二阶非谐效应的偶极贡献

矫正项 $\partial^2 \mu_\gamma / \partial Q_i^2 \cdot \langle Q_i^2 \rangle (T)$ 以维度为 $(i, \alpha)$ 的矩阵表示如下：

- `dip_anharm_2`：$T = 0$ 的情形
- `dipT_anharm_2`：$T = T_{2500}$ 的情形

```python
dip_anharm_2 = 0.5 * dip_2.diagonal(0, 0, 1).T * Q_2[:, None]
dip_anharm_2
```

    array([[ 0.      ,  0.      , -0.      ],
           [ 0.002213, -0.001139, -0.000712],
           [-0.004645,  0.001438,  0.000898],
           [ 0.008767,  0.001811, -0.000082],
           [-0.003475,  0.002666,  0.000128],
           [-0.002509, -0.005047, -0.000059]])

```python
dipT_anharm_2 = 0.5 * dip_2.diagonal(0, 0, 1).T * QT_2[:, None]
dipT_anharm_2
```

    array([[ 0.      ,  0.      , -0.      ],
           [ 0.004927, -0.002536, -0.001586],
           [-0.0092  ,  0.002849,  0.001778],
           [ 0.016485,  0.003405, -0.000155],
           [-0.004312,  0.003309,  0.000159],
           [-0.002791, -0.005614, -0.000066]])

### 一阶振动非谐效应的偶极贡献

矫正项 $\partial \mu_\gamma / \partial Q_i \cdot \langle Q_i \rangle^\mathrm{vib} (T)$ 以维度为 $(i, \alpha)$ 的矩阵表示如下：

- `dip_anharm_1`：$T = 0$ 的情形
- `dipT_anharm_1`：$T = T_{2500}$ 的情形

```python
dip_anharm_1 = dip_1 * Q_1[:, None]
dip_anharm_1
```

    array([[-0.      , -0.      , -0.      ],
           [-0.000133,  0.004004, -0.000545],
           [-0.004411,  0.005262,  0.000769],
           [-0.013259, -0.008027, -0.000521],
           [ 0.002164, -0.000338,  0.000207],
           [ 0.000337,  0.000047,  0.000094]])

```python
dipT_anharm_1 = dip_1 * QT_1[:, None]
dipT_anharm_1
```

    array([[-0.      , -0.      , -0.      ],
           [-0.000242,  0.007286, -0.000992],
           [-0.007703,  0.009188,  0.001343],
           [-0.024782, -0.015004, -0.000973],
           [ 0.003114, -0.000487,  0.000299],
           [ 0.000342,  0.000048,  0.000095]])

:::{danger}

Gaussian 16 rev. B01 与 Gaussian 09 rev. D01 在一阶振动计算上都存在问题，且问题均为计算性质时所用的 $\langle Q_i \rangle^\mathrm{vib} (T)$ 与先前计算的结果不同。

我们回顾到，对于实数频率，

$$
\langle Q_i \rangle^\mathrm{vib} (T) = - \frac{\hbar}{4 \lambda_i} \sum_j \frac{\Phi_{ijj}}{\sqrt{|\lambda_j|}} \theta_j (T)
$$

但在 Gaussian 中的实现，很有可能是

$$
\langle Q_i \rangle^\mathrm{vib} (T) = - \frac{\hbar}{4 \lambda_i} \sum_j \frac{\Phi_{ijj}}{\sqrt{|\lambda_j|}} \theta_j (298.15 \ \mathrm{K}) \theta_j (T)
$$

我认为这显然有些问题哈。

下面被折叠的 code cell 可以复现 Gaussian 输出的结果。

:::

```python
def dip_anharm_1_Gaussian():
    Q_1, QT_1, QTT_1 = np.zeros(nvib), np.zeros(nvib), np.zeros(nvib)
    for i in range(nvib):
        if fa.freq[i] <= 0: continue
        for j in range(nvib):
            val = - deriv_3[i,j,j] / (4 * lambd[i] * np.sqrt(np.abs(lambd[j]))) * Q_scale
            Q_1[i] += val
            QT_1[i] += val * get_coef_temp(298.15)[j]
            QTT_1[i] += val * get_coef_temp(298.15)[j] * get_coef_temp(T)[j]
    print("Gaussian 16 rev. B01")
    print("Temperature: 0 K")
    print(dip_1 * QT_1[:, None])
    print("Temperature: 2500 K")
    print(dip_1 * QTT_1[:, None])
    print("=====")
    print("Gaussian 09 rev. D01")
    print("Temperature: 0 K")
    print(dip_1 * QT_1[:, None] @ fa.rot_vec.T)
    print("Temperature: 2500 K")
    print(dip_1 * QTT_1[:, None] @ fa.rot_vec.T)
dip_anharm_1_Gaussian()
```

    Gaussian 16 rev. B01
    Temperature: 0 K
    [[-0.       -0.       -0.      ]
     [-0.000119  0.003575 -0.000487]
     [-0.004617  0.005507  0.000805]
     [-0.013046 -0.007899 -0.000512]
     [ 0.002838 -0.000443  0.000272]
     [ 0.000674  0.000094  0.000188]]
    Temperature: 2500 K
    [[-0.       -0.       -0.      ]
     [-0.000242  0.007287 -0.000993]
     [-0.007704  0.009189  0.001343]
     [-0.024786 -0.015006 -0.000973]
     [ 0.003113 -0.000487  0.000299]
     [ 0.000341  0.000048  0.000095]]
    =====
    Gaussian 09 rev. D01
    Temperature: 0 K
    [[ 0.        0.        0.      ]
     [-0.001651  0.000601  0.003154]
     [-0.006357 -0.001267  0.003206]
     [-0.008541 -0.001726 -0.012527]
     [ 0.002775  0.000134  0.000776]
     [ 0.000591 -0.000082  0.000378]]
    Temperature: 2500 K
    [[ 0.        0.        0.      ]
     [-0.003365  0.001224  0.006428]
     [-0.010607 -0.002114  0.00535 ]
     [-0.016227 -0.003279 -0.0238  ]
     [ 0.003045  0.000147  0.000851]
     [ 0.000299 -0.000042  0.000191]]

```python
! head -n 13449 anharm-G16B01.log | tail -n 20
```

       ## Vibrational contributions to averages at    0K (Unit: Debye) ##
     ---------------------------------------------------------------------------
              Mode |   P1(diag)     P1(vib)    P1(rot)        P2     |  P1(tot)+P2
     ---------------------------------------------------------------------------
     X           2 |   0.111D-02   0.674D-03   0.000D+00  -0.251D-02 |  -0.183D-02
     X           3 |   0.316D-02   0.284D-02   0.000D+00  -0.348D-02 |  -0.637D-03
     X           4 |  -0.898D-02  -0.130D-01   0.000D+00   0.877D-02 |  -0.428D-02
     X           5 |   0.664D-05  -0.462D-02   0.000D+00  -0.465D-02 |  -0.926D-02
     X           6 |   0.226D-04  -0.119D-03   0.000D+00   0.221D-02 |   0.209D-02
     Y           2 |   0.156D-03   0.944D-04   0.000D+00  -0.505D-02 |  -0.495D-02
     Y           3 |  -0.494D-03  -0.443D-03   0.000D+00   0.267D-02 |   0.222D-02
     Y           4 |  -0.544D-02  -0.790D-02   0.000D+00   0.181D-02 |  -0.609D-02
     Y           5 |  -0.793D-05   0.551D-02   0.000D+00   0.144D-02 |   0.694D-02
     Y           6 |  -0.681D-03   0.357D-02   0.000D+00  -0.114D-02 |   0.244D-02
     Z           2 |   0.310D-03   0.188D-03   0.000D+00  -0.591D-04 |   0.129D-03
     Z           3 |   0.303D-03   0.272D-03   0.000D+00   0.128D-03 |   0.400D-03
     Z           4 |  -0.352D-03  -0.512D-03   0.000D+00  -0.824D-04 |  -0.595D-03
     Z           5 |  -0.116D-05   0.805D-03   0.000D+00   0.898D-03 |   0.170D-02
     Z           6 |   0.927D-04  -0.487D-03   0.000D+00  -0.712D-03 |  -0.120D-02
     ---------------------------------------------------------------------------

```python
! head -n 13479 anharm-G16B01.log | tail -n 20
```

       ## Vibrational contributions to averages at 2500K (Unit: Debye) ##
     ---------------------------------------------------------------------------
              Mode |   P1(diag)     P1(vib)    P1(rot)        P2     |  P1(tot)+P2
     ---------------------------------------------------------------------------
     X           2 |   0.111D-02   0.342D-03  -0.696D-05  -0.279D-02 |  -0.246D-02
     X           3 |   0.316D-02   0.311D-02   0.252D-04  -0.431D-02 |  -0.117D-02
     X           4 |  -0.898D-02  -0.248D-01  -0.115D-03   0.165D-01 |  -0.841D-02
     X           5 |   0.664D-05  -0.770D-02   0.656D-03  -0.920D-02 |  -0.162D-01
     X           6 |   0.226D-04  -0.242D-03   0.104D-04   0.493D-02 |   0.470D-02
     Y           2 |   0.156D-03   0.478D-04  -0.974D-06  -0.561D-02 |  -0.557D-02
     Y           3 |  -0.494D-03  -0.487D-03  -0.394D-05   0.331D-02 |   0.282D-02
     Y           4 |  -0.544D-02  -0.150D-01  -0.698D-04   0.340D-02 |  -0.117D-01
     Y           5 |  -0.793D-05   0.919D-02  -0.782D-03   0.285D-02 |   0.113D-01
     Y           6 |  -0.681D-03   0.729D-02  -0.167D-03  -0.254D-02 |   0.458D-02
     Z           2 |   0.310D-03   0.950D-04  -0.194D-05  -0.658D-04 |   0.273D-04
     Z           3 |   0.303D-03   0.299D-03   0.242D-05   0.159D-03 |   0.460D-03
     Z           4 |  -0.352D-03  -0.973D-03  -0.452D-05  -0.155D-03 |  -0.113D-02
     Z           5 |  -0.116D-05   0.134D-02  -0.114D-03   0.178D-02 |   0.301D-02
     Z           6 |   0.927D-04  -0.992D-03   0.228D-04  -0.159D-02 |  -0.256D-02
     ---------------------------------------------------------------------------

### 一阶转动非谐效应的偶极贡献

我们现在考虑一阶转动的非谐效应。实际上，依据 Harding et al. [^Gauss.JCP.2008] eq. (4)，一阶简谐模量的平均偏移不止有振动的贡献，也还有转动的贡献：

$$
\langle Q_i \rangle (T) = \langle Q_i \rangle^\mathrm{vib} (T) + \langle Q_i \rangle^\mathrm{rot} (T) = - \frac{\hbar}{4 \lambda_i} \sum_j \frac{\Phi_{ijj}}{\sqrt{|\lambda_j|}} \theta_j (T) + \frac{k_\mathrm{B} T}{2 \lambda_i} \sum_\alpha \frac{\partial I_{\alpha \alpha}}{\partial Q_i} \frac{1}{I_{\alpha \alpha}}
$$

为此，我们还需要求取转动惯量 $I_{\alpha \beta}$ 在简振坐标下的导数 $\partial I_{\alpha \beta}$ `deriv_inertia` (维度：$(i, \alpha, \beta)$)。需要注意的是，尽管我们已经将分子转动到 Eckart Orientation 以使得转动惯量矩阵是对角矩阵 $I_{\alpha \beta} = \delta_{\alpha \beta} I_{\alpha \alpha}$，但这不意味着其简振坐标下的导数还是对角的。

```python
def get_fa(mf_hess):
    mol = mf_hess.mol
    fa = FreqAnal()
    fa.mol_weights = get_atom_mass_list(mol)
    fa.mol_coords = mol.atom_coords()
    fa.natm = mol.natm
    fa.mol_hess = mf_hess.de.swapaxes(1, 2)
    return fa
```

```python
deriv_inertia = NumericDiff(num_hess, lambda mf_hess: get_fa(mf_hess).mom_inertia).derivative
deriv_inertia = einsum("Aγδ, γα, δβ -> Aαβ", deriv_inertia, fa.rot_vec, fa.rot_vec)
```

依据这个导数，在合理的单位转换下，我们能给出 $\langle Q_i \rangle^\mathrm{vib} (T_{2500})$ `QTrot_1`。同时，我们也能看到，显然地 $\langle Q_i \rangle^\mathrm{vib} (0)$ `Qrot_1` 为零，因此在 $0 \ \mathrm{K}$ 下，分子旋转对非谐效应不产生贡献。

```python
QTrot_1 = (1/2 * T * k_B / lambd * (deriv_inertia.diagonal(0, -1, -2) / (fa.rot_eig)).sum(axis=-1)) * E_h**-1
QTrot_1[lambd < 0] = 0
Qrot_1 = np.zeros_like(QTrot_1)
QTrot_1
```

    array([ 0.      ,  0.005706, -0.006855, -0.025615,  0.006727,  0.003773])

矫正项 $\partial \mu_\gamma / \partial Q_i \cdot \langle Q_i \rangle^\mathrm{rot} (T)$ 以维度为 $(i, \alpha)$ 的矩阵表示如下：

- `dip_anharm_rot`：$T = 0$ 的情形 (实际上 $0 \ \mathrm{K}$ 不产生贡献)
- `dipT_anharm_rot`：$T = T_{2500}$ 的情形

```python
dipT_anharm_rot = QTrot_1[:, None] * dip_1
dip_anharm_rot = np.zeros_like(dipT_anharm_rot)
dipT_anharm_rot
```

    array([[-0.      , -0.      , -0.      ],
           [-0.000052,  0.001562, -0.000213],
           [-0.000949,  0.001132,  0.000166],
           [-0.005677, -0.003437, -0.000223],
           [ 0.000935, -0.000146,  0.00009 ],
           [ 0.000239,  0.000033,  0.000066]])

旋转的贡献一般来说总是比振动小不少。在 Gaussian 09 版本中，实际上就没有考虑转动的贡献；尽管不太精确，但作这种忽略也应认为是合理的。

:::{danger}

Gaussian 16 rev. B01 的转动贡献计算很可能是错误的。该错误不容易重现。

:::

### 统合所有贡献

回顾到非谐性质在简振坐标下的展开式

$$
\langle P \rangle^\mathrm{anharm} (T) \simeq P + \sum_i \frac{\partial P}{\partial Q_i} \cdot \langle Q_i \rangle (T) + \frac{1}{2} \sum_{i} \frac{\partial^2 P}{\partial Q_i^2} \cdot \langle Q_i^2 \rangle (T)
$$

在所有贡献量都已经求得的情况下，我们很容易地给出 $0 \ \mathrm{K}$ 与 $T_{2500} = 2500 \ \mathrm{K}$ 的非谐矫正下的偶极矩。

未矫正的偶极矩：

```python
print("Dipole/Debye (Uncorrected, Eckart Orientation): ", dip_0)
print("Dipole/Debye (Uncorrected, Input  Orientation): ", dip_0 @ fa.rot_vec.T)
```

    Dipole/Debye (Uncorrected, Eckart Orientation):  [-0.059826  0.052123  0.522057]
    Dipole/Debye (Uncorrected, Input  Orientation):  [-0.013591 -0.522779  0.07319 ]

$0 \ \mathrm{K}$ 的非谐矫正偶极矩：

```python
dip_anharm = dip_0 + (dip_anharm_1 + dip_anharm_rot + dip_anharm_2).sum(axis=0)
print("Dipole/Debye (0 K, Eckart Orientation): ", dip_anharm)
print("Dipole/Debye (0 K, Input  Orientation): ", dip_anharm @ fa.rot_vec.T)
```

    Dipole/Debye (0 K, Eckart Orientation):  [-0.074777  0.052801  0.522232]
    Dipole/Debye (0 K, Input  Orientation):  [-0.027333 -0.525138  0.067748]

$T_{2500} = 2500 \ \mathrm{K}$ 的非谐矫正偶极矩：

```python
dipT_anharm = dip_0 + (dipT_anharm_1 + dipT_anharm_rot + dipT_anharm_2).sum(axis=0)
print("Dipole/Debye (0 K, Eckart Orientation): ", dipT_anharm)
print("Dipole/Debye (0 K, Input  Orientation): ", dipT_anharm @ fa.rot_vec.T)
```

    Dipole/Debye (0 K, Eckart Orientation):  [-0.089493  0.053711  0.521843]
    Dipole/Debye (0 K, Input  Orientation):  [-0.041028 -0.526894  0.062559]

## 非谐谱学性质：红外光谱

如果上面用于计算偶极矩的计算方法也可以用于计算偶极矩的坐标导数，那么红外光谱也可以通过类似的方法实现。

:::{danger}

这只是依葫芦画瓢，用计算非谐偶极的方法套用到红外光谱中。很显然这未必是正确的，而且事实上，不仅是非谐振动频率 (因为 Fermi 或 Darling-Dennsion 共振)，**下述代码的 IR 峰强度结果也与 Gaussian 的输出非常不同**。

因此，各位看官就当看个热闹哈 (｀・ω・´)

想要了解更多信息，请参考 Bloino 作为第一作者的文章 ([^Bloino.JCP.2012], [^Bloino.JPCA.2015])。红外光谱应当需要作更严格的 RSPT 二阶展开，并且考虑一部分非基频振动间的相互跃迁偶极矩导数。这部分工作应当已经实现在 Gaussian 16 rev. B01 中，因此我们能获得非谐红外信息。

在上面对 Gaussian 程序的实现结果作讨论之后，我其实觉得务实看待 Gaussian 输出的态度应当是保留地接受它。它所给出的分子振动频率还是相对准确的，特别是相比与本文档而言，对频率共振的处理多少让结果变好一些 (尽管对于 Fermi 共振，如果不应用 DCPT2 或其它简并微扰理论而完全去除共振项，任意性其实太强；这在 Gaussian 中或许实现了，但我还不清楚)。但在偶极矩 (或可能地，磁矩等) 的非谐处理上，大概还有需改进之处。

:::

我们这里选择使用相对来说较为真实的乙醛分子 (CH<sub>3</sub>CHO) 分子，作为绘制红外光谱的分子。计算方法仍然是 STO-3G。由于我们需要重新写一遍近乎完整的代码来绘制红外光谱，因此这里将大部分代码进行了折叠。其输入卡 {download}`CH3CHO.gjf`，输出文件 {download}`CH3CHO.log`。绘制红外光谱时，使用 Lorentzian 展宽，半峰宽 $30 \ \mathrm{cm^{-1}}$。

### 分子定义与原子核高阶导数

```python
mol = gto.Mole(atom="""
C   -0.852693    0.499814   -0.000294
C    0.685096    0.670458    0.000342
O    1.340323   -0.602319   -0.000432
O   -1.349649   -0.611839    0.000139
H   -1.450829    1.425645   -0.001000
H    0.974215    1.253523    0.883979
H    0.975003    1.254674   -0.882236
H    0.581799   -1.242205    0.001322
""", basis="STO-3G", verbose=0).build()
```

```python
natm = mol.natm
nhess = natm * 3
mf = scf.RHF(mol).run()
mf_hess = mf.Hessian().run()
```

```python
fa = FreqAnal()
fa.mol_weights = get_atom_mass_list(mol)
fa.mol_coords = mol.atom_coords()
fa.natm = mol.natm
fa.mol_hess = mf_hess.de.swapaxes(1, 2)
nvib = fa.freq.size
```

```python
deriv_2 = einsum("Pi, PQ, Qj -> ij", fa.q, fa.mol_hess.reshape(nhess, nhess), fa.q)
lambd = deriv_2.diagonal()
```

```python
num_hess = ModeDerivGenerator(mol, lambda mol: scf.RHF(mol).run().Hessian().run(), fa.q, interval=0.01)
tmp_3 = NumericDiff(num_hess, lambda mf: mf.de.swapaxes(1, 2).reshape(nhess, nhess)).derivative
deriv_3 = einsum("Ai, Bj, kAB -> ijk", fa.q, fa.q, tmp_3)
deriv_3 = 1/3 * (deriv_3 + deriv_3.transpose(1, 2, 0) + deriv_3.transpose(2, 0, 1))
```

```python
hess_gathered = np.array([[mf.de.swapaxes(1, 2).reshape(nhess, nhess) for mf in l] for l in num_hess.objects])
tmp_4 = einsum("ksAB, Ai, Bj -> skij", hess_gathered, fa.q, fa.q)
deriv_4 = (tmp_4[0] + tmp_4[1] - 2 * deriv_2) / (num_hess.interval**2)  # finite diff
deriv_4 = (deriv_4 + deriv_4.swapaxes(-1, -2)) / 2                      # symmetrize
deriv_4 = einsum("kij -> ijk", deriv_4)                                 # subscript transform
```

### $\xi_{ij}$ 矩阵生成与非谐频率计算

```python
xmat_4 = np.zeros((nvib, nvib))
for i in range(nvib):
    xmat_4[i, i] = deriv_4[i,i,i] / (16 * lambd[i])
    for j in range(nvib):
        if i == j: continue
        xmat_4[i, j] = deriv_4[i,i,j] / (4 * np.sqrt(np.abs(lambd[i] * lambd[j])))
xmat_4 *= (hbar / (2 * np.pi * c_0)) * (a_0**-2 * amu**-1) / (100)
```

```python
xmat_3 = np.zeros((nvib, nvib))
for i in range(nvib):
    xmat_3[i, i] -= deriv_3[i,i,i]**2 * 5 / (3 * lambd[i])
    for j in range(nvib):
        if i == j: continue
        xmat_3[i, i] -= deriv_3[i,i,j]**2 * (8 * lambd[i] - 3 * lambd[j]) / (lambd[j] * (4 * lambd[i] - lambd[j]))
    xmat_3[i, i] /= 16 * lambd[i]
for i in range(nvib):
    for j in range(nvib):
        if i == j: continue
        xmat_3[i, j] -= deriv_3[i,i,j]**2 * 2 / (4 * lambd[i] - lambd[j])
        xmat_3[i, j] -= deriv_3[i,j,j]**2 * 2 / (4 * lambd[j] - lambd[i])
        xmat_3[i, j] -= deriv_3[i,i,i] * deriv_3[i,j,j] / lambd[i]
        xmat_3[i, j] -= deriv_3[j,j,j] * deriv_3[i,i,j] / lambd[j]
        for k in range(nvib):
            if len(set([i, j, k])) != 3: continue
            delta_ijk = lambd[i]**2 + lambd[j]**2 + lambd[k]**2 - 2 * (lambd[i]*lambd[j] + lambd[j]*lambd[k] + lambd[k]*lambd[i])
            xmat_3[i, j] += deriv_3[i,j,k]**2 * 2 * (lambd[i] + lambd[j] - lambd[k]) / delta_ijk
            xmat_3[i, j] -= deriv_3[i,i,k] * deriv_3[j,j,k] / lambd[k]
        xmat_3[i, j] /= 4 * np.sqrt(np.abs(lambd[i] * lambd[j]))
xmat_3 *= (hbar / (2 * np.pi * c_0)) * (a_0**-2 * amu**-1) / (100)
```

```python
rot_wavenum = h / (8 * np.pi**2 * c_0 * fa.rot_eig) * (a_0**-2 * amu**-1) / (100)
q_ = einsum("Aβi, βα, A -> Aαi", fa.q.reshape(natm, 3, nvib), fa.rot_vec, np.sqrt(get_atom_mass_list(mol)))
zeta_x = einsum("Ai, Aj -> ij", q_[:, 1, :], q_[:, 2, :]) - einsum("Ai, Aj -> ij", q_[:, 2, :], q_[:, 1, :])
zeta_y = einsum("Ai, Aj -> ij", q_[:, 2, :], q_[:, 0, :]) - einsum("Ai, Aj -> ij", q_[:, 0, :], q_[:, 2, :])
zeta_z = einsum("Ai, Aj -> ij", q_[:, 0, :], q_[:, 1, :]) - einsum("Ai, Aj -> ij", q_[:, 1, :], q_[:, 0, :])
zeta = np.array([zeta_x, zeta_y, zeta_z])

xmat_coriol = np.zeros((nvib, nvib))
for i in range(nvib):
    for j in range(nvib):
        for ixyz in range(3):
            xmat_coriol[i, j] += zeta[ixyz, i, j]**2 * rot_wavenum[ixyz]
        xmat_coriol[i, j] *= (lambd[i] + lambd[j]) / np.sqrt(np.abs(lambd[i] * lambd[j]))
```

```python
xmat = xmat_4 + xmat_3 + xmat_coriol
freq_anharm = fa.freq + 1.5 * xmat.diagonal() + 0.5 * xmat.sum(axis=-1)
```

### 温度效应矫正系数计算 (2500 K)

```python
T = 2500
coef_temp = get_coef_temp(T)
Q_scale = hbar / np.sqrt(E_h * a_0**2 * amu)
```

```python
Q_1, QT_1 = np.zeros(nvib), np.zeros(nvib)
for i in range(nvib):
    if fa.freq[i] <= 0: continue
    for j in range(nvib):
        val = - deriv_3[i,j,j] / (4 * lambd[i] * np.sqrt(np.abs(lambd[j]))) * Q_scale
        Q_1[i] += val
        QT_1[i] += val * coef_temp[j]
```

```python
Q_2, QT_2 = np.zeros(nvib), np.zeros(nvib)
for i in range(nvib):
    if fa.freq[i] <= 0: continue
    val = 1 / (2 * np.sqrt(lambd[i])) * Q_scale
    Q_2[i] += val
    QT_2[i] += val * coef_temp[i]
```

```python
deriv_inertia = NumericDiff(num_hess, lambda mf_hess: get_fa(mf_hess).mom_inertia).derivative
deriv_inertia = einsum("Aγδ, γα, δβ -> Aαβ", deriv_inertia, fa.rot_vec, fa.rot_vec)
QTrot_1 = (1/2 * T * k_B / lambd * (deriv_inertia.diagonal(0, -1, -2) / (fa.rot_eig)).sum(axis=-1)) * E_h**-1
QTrot_1[lambd < 0] = 0
Qrot_1 = np.zeros_like(QTrot_1)
```

### 偶极矩导数与红外强度计算

```python
dip_0 = mf.dip_moment() @ fa.rot_vec
dip_1 = einsum("Aα, αγ, Ai -> iγ", get_dipderiv(mf_hess), fa.rot_vec, fa.q) * data.nist.AU2DEBYE
dip_tmp_2 = NumericDiff(num_hess, lambda mf_hess: get_dipderiv(mf_hess)).derivative * data.nist.AU2DEBYE
dip_2 = einsum("iAα, αγ, Aj -> ijγ", dip_tmp_2, fa.rot_vec, fa.q)
dipderiv_gathered = np.array([[get_dipderiv(mf_hess) for mf_hess in l] for l in num_hess.objects]) * data.nist.AU2DEBYE
dip_tmp_3 = einsum("iσAα, αγ, Aj -> σijγ", dipderiv_gathered, fa.rot_vec, fa.q)
dip_3 = (dip_tmp_3[0] + dip_tmp_3[1] - 2 * dip_1) / (num_hess.interval**2)
```

    Dipole moment(X, Y, Z, Debye): -0.85484,  1.70251,  0.00242

```python
IR_scale = 1/3 * np.pi * F**2 * data.nist.AU2DEBYE**-2 * 1e-7
IR_inten = (dip_1**2).sum(axis=-1) * IR_scale
```

```python
dip_1_anharm = einsum("ijγ, j -> jγ", dip_2, Q_1 + Qrot_1) + 0.5 * einsum("ijγ, j -> jγ", dip_3, Q_2)
IR_anharm = ((dip_1 + dip_1_anharm)**2).sum(axis=-1) * IR_scale
```

```python
dip_1T_anharm = einsum("ijγ, j -> jγ", dip_2, QT_1 + QTrot_1) + 0.5 * einsum("ijγ, j -> jγ", dip_3, QT_2)
IRT_anharm = ((dip_1 + dip_1T_anharm)**2).sum(axis=-1) * IR_scale
```

### 红外光谱绘制

```python
def lorentzian_freq(omega, omega_n, gamma):
    return 100 * 0.5 / np.pi * gamma / ((omega - omega_n)**2 + 0.25 * gamma**2)
```

```python
def ir_plot(omega, gamma, freq, ir):
    val = 0
    assert(len(freq) == len(ir))
    for i in range(len(freq)):
        val += lorentzian_freq(omega, freq[i], gamma) * ir[i]
    return val
```

```python
freq_anharm_gau = [4098.800, 3570.604, 3446.746, 3471.841, 2088.048, 1787.600, 1725.346, 1593.897, 1530.767, 1407.142, 1297.110, 1206.649, 1019.526, 878.962, 795.699, 185.138, 258.388, 69.977]
IR_anharm_gau = [2.37219675, 1.54485433, 9.30810815, 7.71760501, 22.32308331, 0.91999625, 21.74763306, 0.33568248, 39.89748639, 0.34998748, 8.84110221, 1.90263087, 8.44102685, 0.12421802, 12.58976516, 36.74388344, 9.13647155, 24.14833335]
```

```python
fig, ax = plt.subplots(figsize=(7, 4))
ax.grid()

x = np.arange(0, 4500, 1)
ax.set_xlim(0, 4500)
ax.set_ylim(-10, 180)
ax.plot(x, ir_plot(x, 30, fa.freq, IR_inten), label="Harmonic")
ax.plot(x, ir_plot(x, 30, freq_anharm, IR_anharm), label="Anharmonic (0 K)")
# ax.plot(x, ir_plot(x, 30, freq_anharm, IRT_anharm), label="Anharmonic (2500 K)")
ax.plot(x, ir_plot(x, 30, freq_anharm_gau, IR_anharm_gau), label="Anharmonic (Gaussian)")
ax.set_ylabel("Molar Absorption Coefficient (L mol$^{-1}$ cm$^{-1}$)")
ax.set_xlabel("Vibration Wavenumber (cm$^{-1}$)")
ax.set_title("Acetaldehyde ($\mathsf{CH_3CHO}$) Infared Spectrum (HF/STO-3G)")
ax.legend(loc="upper right")

ax.set_ylabel("IR Intensity (km mol$^{-1}$)")
ax.legend(loc="upper right")
```

    <IPython.core.display.Javascript object>

    <matplotlib.legend.Legend at 0x7f5974689ee0>

## 文档补充信息

这份文档的一部分内容与颜文杰有较多讨论，与申同昊有一部分讨论。我也算是对非谐矫正有些好奇，于是尝试搞了一下。

[^Barone.JCP.2005]: Anharmonic Vibrational Properties by a fully automated second-order perturbative approach, V. Barone, *J. Chem. Phys.* **2005**, *122*, 014108, doi: [10.1063/1.1824881](https://doi.org/10.1063/1.1824881).

[^Miller.CPL.1990]: Ab initio calculation of anharmonic constants for a transition state, with application to semiclassical transition state tunneling probabilities, W. H. Miller, R. Hernandez, N. C. Handy, D. Jayatilaka, A. Willetts, *Chem. Phys. Lett.* **1990**, *172*, 62-68, doi: [10.1016/0009-2614(90)87217-F](https://doi.org/10.1016/0009-2614(90)87217-F).

[^Gauss.JCP.2008]: Quantitative prediction of gas-phase <sup>19</sup>F nuclear magnetic shielding constants, M. E. Harding, M. Lenhart, A. A. Auer, J. Gauss, *J. Chem. Phys.* **2008**, *128*, 244111, doi: [10.1063/1.2943145](https://doi.org/10.1063/1.2943145).

[^Bloino.JCP.2012]: A second-order perturbation theory route to vibrational averages and transition properties of molecules: General formulation and application to infrared and vibrational circular dichroism spectroscopies, J. Bloino, V. Barone, *J. Chem. Phys.* **2012**, *136*, 124108, doi: [10.1063/1.3695210](https://doi.org/10.1063/1.3695210).

[^Bloino.JPCA.2015]: A VPT2 Route to Near-Infrared Spectroscopy: The Role of Mechanical and Electrical Anharmonicity, *J. Phys. Chem. A* **2015**, *119*, 5269-5287, doi: [10.1021/jp509985u](https://doi.org/10.1021/jp509985u).


```python
%matplotlib inline
```

적대적 예제 생성(Adversarial Example Generation)
====================================================

**저자:** `Nathan Inkawhich <https://github.com/inkawhich>`__
**번역:** `BONGMO KIM <https://github.com/bongmo>`__

이 글을 읽고 있다면, 여러분은 이미 머신러닝 모델이 얼마나 효과적인지 그 진가를 알고 있을 것입니다.
머신 러닝 연구는 ML(Machine Learning) 모델을 더욱 빠르고 정확하며 효율적이게 하는 방향으로 진행 되고 있습니다.
그러나 모델을 속이려하는 적에 대한 보안과 견고함은 모델을 설계하고 훈련할 때 종종 간과되는 부분입니다.

이 튜토리얼은 ML 모델들의 보안 취약점에 대한 인식을 높이고, 요즘 화두가 되고있는 적대적 머신 러닝에 대한 통찰력을 제공할 것입니다.
이미지에 눈치챌 수 없는 작은 변화(perturbation)를 추가하면 모델 성능이 크게 달라질 수 있다는 사실에 놀랄 수 있습니다.
이번 튜토리얼에서는 이미지 분류기의 예제를 통해 위 내용에 대해 살펴볼 것입니다.
특히 우리는 가장 많이 사용되는 공격 방법 중 하나인 FGSM (Fast Gradient Sign Attack)을 이용해 MNIST 분류기를 속여 볼 것입니다.

위협 모델
------------

상황에 따라 다양한 범주의 적대적 공격이 있는데 각각 목표가 다르고 공격자가 알고 있는 정보
대한 가정도 다릅니다. 그러나 보통 가장 중요한 목표는 입력 데이터에 최소한의 작은 변화를
추가하여 이것이 의도적으로 잘못 분류되게 하는 것입니다. 공격자가 가지고 있는 정보에 대한
가정에는 여러 종류가 있는데, 보통 **화이트박스** 와 **블랙박스** 두 가지가 있습니다.
*화이트박스* 공격은 공격자가 모델에 대해 아키텍처, 입력, 출력, 가중치를 포함한 모든 것을
알고 있고 접근할 수 있다고 가정합니다. *블랙박스* 공격은 공격자가 모델의 입력과 출력에
대해서만 접근 가능하고 모델의 가중치와 아키텍처에 관한 내용은 모른다고 가정합니다.
공격자의 목표는 오분류 및 **소스/타겟 오분류** 를 포함하는 여러 유형이 있습니다.
*오분류* 의 목표는 공격자가 출력으로 나온 분류 결과가 잘못 되도록 하나 새로운 분류 결과가
어떤 것이 나오는지 신경 쓰지 않는 것을 의미합니다. *소스/타겟 오분류* 는 공격자가
원래 특정 소스 클래스의 이미지를 다른 특정 대상 클래스로 분류하도록 변경하려고 함을 의미합니다.

이 경우 FGSM 공격은 *오분류* 를 목표로 하는 화이트 박스 공격입니다.
이런 배경 정보를 갖고 공격에 대해 자세히 알아 보겠습니다.

빠른 변화도 부호 공격
-------------------------

공격 방법에 있어 초기 방식이면서 가장 유명한 방식은 *빠른 변화도 부호 공격 (FGSM)* 이라고 하며
`적대적 예제에 대한 설명과 활용 <https://arxiv.org/abs/1412.6572>`__ 에서
이안 갓펠로우가 기고하였습니다.
이 공격법은 놀랍도록 강력하지만 직관적입니다. 학습 방식, *변화도(gradients)* 를 활용하여 신경망을 공격하도록
설계 되었습니다. 아이디어는 간단합니다. 역전파 변화도를 기반으로 가중치를 조정하여 손실을 최소화하기보다는
공격이 동일한 역전파 변화도를 기반으로 *손실을 최대화하하는 방향으로 입력 데이터를 조정* 합니다.
다시 말해 공격은 입력 데이터에서 계산된 손실 변화도를 사용하고 입력 데이터를 조정하여 손실이 최대가 되게 합니다.
코드로 넘어가기 전에 유명한 `FGSM <https://arxiv.org/abs/1412.6572>`__ 판다 예제를
보고 몇 가지 표기법을 정리하겠습니다.

.. figure:: /_static/img/fgsm_panda_image.png
   :alt: fgsm_panda_image

그림으로부터, $\mathbf{x}$ 는 원본 입력 이미지가 "판다" 로 올바르게 분류된 것을 의미하고,
$y$ 는 $\mathbf{x}$ 를 위한 정답 라벨이며, $\mathbf{\theta}$ 는 모델의
파라미터를, $J(\mathbf{\theta}, \mathbf{x}, y)$ 는 네트워크의 학습을 위해서 사용되는 손실을 나타냅니다.
공격은 $\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$ 계산을 위해 입력 데이터에 변화도를 역전파합니다.
그러고 나서, 변화도는 손실 값이 최대화되는 방향으로 (예를 들면, $sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))$ )
작은 스텝(step) 만큼 (그림에서는 $\epsilon$ 혹은 $0.007$) 입력 데이터에 적용됩니다.
결과로 나오는 작은 변화된 이미지( $x'$ )는 타겟 네트워크에 의해 "긴팔원숭이"로 *오분류* 되나 여전히 육안으로는
분명히 "판다" 입니다.

이제 본 튜토리얼의 동기가 명확해지길 바라며, 구현으로 넘어가 보겠습니다.

```python
from __future__ import print_function
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import numpy as np
import matplotlib.pyplot as plt

# NOTE: 아래는 MNIST 데이터셋을 내려받을 때 "User-agent" 관련한 제한을 푸는 코드입니다.
#       더 자세한 내용은 https://github.com/pytorch/vision/issues/3497 을 참고해주세요.
from six.moves import urllib
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
```

구현
--------------

이 섹션에서는 튜토리얼의 입력 매개 변수에 대해 설명하고 공격중인 모델을
정의한 다음 공격을 코딩하고 일부 테스트를 실행합니다.

입력
~~~~~~

이 학습서에는 입력이 3 개이며 다음과 같이 정의됩니다:

- **epsilons** - 실행에 사용할 엡실론의 리스트입니다. 엡실론 0의 값은 원래 테스트 셋의 모델 성능을
  나타내므로 목록에 유지하는 것이 중요합니다. 또한 직관적으로 엡실론이 클수록 작은 변화가 더 눈에 띄지만
  모델 정확도를 저하 시키는 측면에서 더 효과가 있습니다. 여기서 데이터의 범위는 0-1 이기 때문에
  엡실론의 값은 1을 초과할 수 없습니다.

- **pretrained_model** - `pytorch/examples/mnist <https://github.com/pytorch/examples/tree/master/mnist>`__
   를 통해 미리 학습된 MNIST 모델의 경로.
   튜토리얼을 간편하게 하려면 `여기 <https://drive.google.com/drive/folders/1fn83DF14tWmit0RTKWRhPq5uVXt73e0h?usp=sharing>`__ 에서 미리 학습된 모델을 다운로드하세요.

-  **use_cuda** - CUDA 를 사용할지 말지 정하는 이진 플래그.
   본 튜토리얼에서는 CPU 시간이 오래 걸리지 않으므로 CUDA를 지원하는 GPU 의 여부는 중요하지 않습니다.

```python
epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "data/lenet_mnist_model.pth"
use_cuda=True
```

공격을 받는 모델
~~~~~~~~~~~~~~~~~~

앞서 말한대로, 공격을 받는 모델은 `pytorch/examples/mnist <https://github.com/pytorch/examples/tree/master/mnist>`__
와 동일한 MNIST 모델입니다. 본인의 MNIST 모델을 학습 및 저장하는 방식으로 하거나 제공된 모델을 다운로드 해 사용하는 식으로 진행할 수 있습니다.
여기서 *Net* 정의 및 테스트 데이터 로더는 MNIST 예제에서 복사 하였습니다.
이 섹션의 목적은 모델과 데이터 로더를 정의한 다음, 모델을 초기화하고 미리 학습된 가중치를 읽어오는 것입니다.

```python
# LeNet 모델 정의
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# MNIST 테스트 데이터셋과 데이터로더 선언
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, download=True, transform=transforms.Compose([
            transforms.ToTensor(),
            ])),
        batch_size=1, shuffle=True)

# 어떤 디바이스를 사용할지 정의
print("CUDA Available: ",torch.cuda.is_available())
device = torch.device("cuda" if (use_cuda and torch.cuda.is_available()) else "cpu")

# 모델 초기화하기
model = Net().to(device)

# 미리 학습된 모델 읽어오기
model.load_state_dict(torch.load(pretrained_model, map_location='cpu'))

# 모델을 평가 모드로 설정하기. 드롭아웃 레이어들을 위해 사용됨
model.eval()
```

FGSM 공격
~~~~~~~~~~~

이제 원래 입력을 교란시켜 적대적인 예를 만드는 함수를 정의 할 수 있습니다.
``fgsm_attack`` 함수는 입력 파라미터로 3가지를 가집니다. 첫번째는 원본 *이미지* ( $x$ ),
두번째는 *엡실론* 으로 픽셀 단위의 작은 변화를 주는 값입니다 ( $\epsilon$ ).
마지막은 *data_grad* 로 입력 영상 ( $\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$ ) 에 대한 변화도 손실 값입니다.
아래 식에 따른 작은 변화가 적용된 이미지를 생성합니다.

\begin{align}perturbed\_image = image + epsilon*sign(data\_grad) = x + \epsilon * sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))\end{align}

마지막으로 데이터의 원래 범위를 유지하기 위해, 작은 변화가 적용된 이미지가 $[0,1]$ 범위로 잘립니다.

```python
# FGSM 공격 코드
def fgsm_attack(image, epsilon, data_grad):
    # data_grad 의 요소별 부호 값을 얻어옵니다
    sign_data_grad = data_grad.sign()
    # 입력 이미지의 각 픽셀에 sign_data_grad 를 적용해 작은 변화가 적용된 이미지를 생성합니다
    perturbed_image = image + epsilon*sign_data_grad
    # 값 범위를 [0,1]로 유지하기 위해 자르기(clipping)를 추가합니다
    perturbed_image = torch.clamp(perturbed_image, 0, 1)
    # 작은 변화가 적용된 이미지를 리턴합니다
    return perturbed_image
```

테스팅 함수
~~~~~~~~~~~~~~~~

마지막으로 본 튜토리얼의 핵심 결과는 ``테스트`` 함수에서 오게 됩니다.
이 테스트 기능을 호출 할 때마다 MNIST 테스트 셋에서 전체 테스트 단계를 수행하고 최종 정확도를 보고합니다.
그러나 이 함수에는 *엡실론* 입력도 필요합니다. 이는 ``테스트``` 함수가 $\epsilon$ 크기에 따라 공격자의 공격을 받는 모델의
정확도을 보고하기 때문입니다. 더 구체적으로 보면 테스트 셋의 각각의 샘플에서 테스트 함수는 입력 데이터에 대한 손실 변화도( $data\_grad$ )를 계산하고,
``FGSM 공격`` ($perturbed\_data$) 을 받은 작은 변화가 적용된 이미지를 만들고 나서 작은 변화가 적용된 이미지가 적대적인지 확인을 합니다.
추가로 모델의 정확도를 테스트하기 위해서 테스트 함수는 나중에 시각화하여 볼 수 있도록 성공적으로 얻은 적대적 이미지를 저장하고 반환합니다.

```python
def test( model, device, test_loader, epsilon ):

    # 정확도 카운터
    correct = 0
    adv_examples = []

    # 테스트 셋의 모든 예제에 대해 루프를 돕니다
    for data, target in test_loader:

        # 디바이스(CPU or GPU) 에 데이터와 라벨 값을 보냅니다
        data, target = data.to(device), target.to(device)

        # 텐서의 속성 중 requires_grad 를 설정합니다. 공격에서 중요한 부분입니다
        data.requires_grad = True

        # 데이터를 모델에 통과시킵니다
        output = model(data)
        init_pred = output.max(1, keepdim=True)[1] # 로그 확률의 최대값을 가지는 인덱스를 얻습니다

        # 만약 초기 예측이 틀리면, 공격하지 않도록 하고 계속 진행합니다
        if init_pred.item() != target.item():
            continue

        # 손실을 계산합니다
        loss = F.nll_loss(output, target)

        # 모델의 변화도들을 전부 0으로 설정합니다
        model.zero_grad()

        # 후방 전달을 통해 모델의 변화도를 계산합니다
        loss.backward()

        # 변화도 값을 모읍니다
        data_grad = data.grad.data

        # FGSM 공격을 호출합니다
        perturbed_data = fgsm_attack(data, epsilon, data_grad)

        # 작은 변화가 적용된 이미지에 대해 재분류합니다
        output = model(perturbed_data)

        # 올바른지 확인합니다
        final_pred = output.max(1, keepdim=True)[1] # 로그 확률의 최대값을 가지는 인덱스를 얻습니다
        if final_pred.item() == target.item():
            correct += 1
            # 0 엡실론 예제에 대해서 저장합니다
            if (epsilon == 0) and (len(adv_examples) < 5):
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) )
        else:
            # 추후 시각화를 위하 다른 예제들을 저장합니다
            if len(adv_examples) < 5:
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) )

    # 해당 엡실론에서의 최종 정확도를 계산합니다
    final_acc = correct/float(len(test_loader))
    print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc))

    # 정확도와 적대적 예제를 리턴합니다
    return final_acc, adv_examples
```

공격 실행
~~~~~~~~~~

구현의 마지막 부분은 공격을 실행하는 것입니다. 여기서 전체 테스트 스텝을 각 *엡실론* 값에 실행합니다.
각 엡실론마다 최종 정확도와 성공적인 일부 적대 사례를 저장하여 다음 섹션에 표시합니다.
엡실론 값이 증가함에 따라 출력된 정확도가 어떻게 감소하는지 보십시오.
또한, $\epsilon=0$ 인 경우에는 공격이 없는 원본 테스트 정확도임을 보입니다.

```python
accuracies = []
examples = []

# 각 엡실론에 대해 테스트 함수를 실행합니다
for eps in epsilons:
    acc, ex = test(model, device, test_loader, eps)
    accuracies.append(acc)
    examples.append(ex)
```

결과
-------

정확도 vs 엡실론
~~~~~~~~~~~~~~~~~~~

첫 번째 결과는 정확도 vs 엡실론 을 도식화 한 것 입니다.
앞에서 언급했듯이, 엡실론이 증가함에 따라 우리는 테스트 정확도가 감소할 것으로 예상합니다.
이는 학습을 더 진행해 갈수록 엡실론이 클수록 손실을 극대화 할 방향으로 진행되기 때문입니다.
엡실론 값이 선형적으로 분포하더라도 곡선의 추세는 선형의 형태가 아닙니다.
예를 들면, math:`\epsilon=0.05` 에서의 정확도가 $\epsilon=0$ 보다 약 4% 낮지만
$\epsilon=0.2$ 에서의 정확도는 $\epsilon=0.15$ 보다 약 25% 정도 낮습니다.
또한, $\epsilon=0.25$ 와 $\epsilon=0.3$ 사이의 모델 정확도는 랜덤으로
10개중 1개를 선택했을 때의 정확도와 유사한 수준입니다.

```python
plt.figure(figsize=(5,5))
plt.plot(epsilons, accuracies, "*-")
plt.yticks(np.arange(0, 1.1, step=0.1))
plt.xticks(np.arange(0, .35, step=0.05))
plt.title("Accuracy vs Epsilon")
plt.xlabel("Epsilon")
plt.ylabel("Accuracy")
plt.show()
```

샘플 적대적 예제들
~~~~~~~~~~~~~~~~~~~~~~~~~~~

공짜 점심은 없다는 것을 기억하시나요? 이 경우에는 엡실론이 증가할수록 테스트 정확도는 떨어집니다.
**그러나** 작은 변화는 더 쉽게 인식할 수 있게 됩니다.
실제로 정확도 저하와 공격자가 고려해야 하는 이해도 사이에는 상충 관계(tradeoff)가 있습니다.
여기서 우리는 각 엡실론 값에서 성공적인 대적 사례를 보이는 몇 가지 예를 보겠습니다.
아래 이미지의 첫번째로 열은 $\epsilon=0$ 인 예제들로 작은 변화가 없는 원본의 "깨끗한" 이미지들을 나타냅니다.
각 이미지의 위의 글자는 "원래 분류 결과 -> 적대적 분류 결과"를 나타냅니다.
$\epsilon=0.15$ 에서 작은 변화가 눈에 띄기 시작하고 $\epsilon=0.3$ 에서는 확실해 보입니다.
그러나 모든 경우에 대해서 노이즈가 추가되었더라도 사람은 올바르게 분류를 수행할 수 있습니다.

```python
# 각 엡실론에서 적대적 샘플의 몇 가지 예를 도식화합니다
cnt = 0
plt.figure(figsize=(8,10))
for i in range(len(epsilons)):
    for j in range(len(examples[i])):
        cnt += 1
        plt.subplot(len(epsilons),len(examples[0]),cnt)
        plt.xticks([], [])
        plt.yticks([], [])
        if j == 0:
            plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14)
        orig,adv,ex = examples[i][j]
        plt.title("{} -> {}".format(orig, adv))
        plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()
```

다음 단계는?
-----------------

이번 튜토리얼에서 적대적 머신 러닝에 대한 통찰을 얻을 수 있었기를 바랍니다.
튜토리얼의 내용으로부터 앞으로 더 많은 것들을 알아나갈 수 있습니다.
튜토리얼의 적대적 공격 예제는 본 분야의 초급 단계이며
적대적 상황으로부터 ML 모델을 공격하고 방어하는 방법에 대한 많은 후속 아이디어가 있습니다.
사실 NIPS 2017 에서 적대적 공격과 방어에 대한 경쟁(competition)이 있었고 여기서 사용된
다양한 방법들은 다음 논문에 정리 되어 있습니다: `적대적 공격과 방어 경쟁 <https://arxiv.org/pdf/1804.00097.pdf>`__.
방어에 대한 연구는 자연스럽게 교란 및 해킹 목적으로 제작된 입력에 대해 머신 러닝 모델을
보다 *견고하게(robust)* 만드는 아이디어로 이어집니다.

또 다른 방향은 다른 도메인에서의 적의 공격과 방어입니다. 적대적 연구는 이미지 도메인에 제한되어 있지 않습니다.
`여기 <https://arxiv.org/pdf/1801.01944.pdf>`__ 에서 음성-텍스트 변환 모델에서의 공격도 확인할 수 있습니다.
그러나 적대적 머신 러닝 분야에 대해서 많은 것을 알기 위한 최고의 방법은 많이 시도해보는 것입니다.
NIPS 2017 경쟁에서 소개된 다양한 공격 방법을 직접 구현해 보고, FGSM 과 어떤 점이 다른지 연구해 보세요.
그리고 나서 직접 만든 공격으로부터 모델을 방어해 보세요.


```python
# This cell is added by sphinx-gallery
# It can be customized to whatever you like
%matplotlib inline
```

Plugins and Hybrid computation
==============================

.. meta::
    :property="og:description": This tutorial introduces the notion of hybrid
        computation by combining several PennyLane device backends to train an algorithm
        containing both photonic and qubit devices.
    :property="og:image": https://pennylane.ai/qml/_images/photon_redirection.png

.. related::

   tutorial_qubit_rotation Basic tutorial: qubit rotation
   tutorial_gaussian_transformation Gaussian transformation

*Author: PennyLane dev team. Last updated: 1 Feb 2021.*

This tutorial introduces the notion of hybrid computation by combining several PennyLane
plugins. We first introduce PennyLane's `Strawberry Fields plugin <https://pennylane-sf.readthedocs.io>`_
and use it to explore a non-Gaussian photonic circuit. We then combine this photonic circuit with a
qubit circuit — along with some classical processing — to create and optimize a fully hybrid computation.
Be sure to read through the introductory `qubit rotation <qubit_rotation>` and
`Gaussian transformation <gaussian_transformation>` tutorials before attempting this tutorial.

Note

To follow along with this tutorial on your own computer, you will require the
    `PennyLane-SF plugin <https://pennylane-sf.readthedocs.io>`_, in order to access the
    `Strawberry Fields <https://strawberryfields.readthedocs.io>`_ Fock backend using
    PennyLane. It can be installed via pip:

    .. code-block:: bash

        pip install pennylane-sf

A non-Gaussian circuit
----------------------

We first consider a photonic circuit which is similar in spirit to the
`qubit rotation <qubit_rotation>` circuit:

.. figure:: ../demonstrations/plugins_hybrid/photon_redirection.png
    :align: center
    :width: 30%
    :target: javascript:void(0);

Breaking this down, step-by-step:

1. **We start the computation with two qumode subsystems**. In PennyLane, we use the
   shorthand 'wires' to refer to quantum subsystems, whether they are qumodes, qubits, or
   any other kind of quantum register.

2. **Prepare the state** $\left|1,0\right\rangle$. That is, the first wire (wire 0) is prepared
   in a single-photon state, while the second
   wire (wire 1) is prepared in the vacuum state. The former state is non-Gaussian,
   necessitating the use of the ``'strawberryfields.fock'`` backend device.

3. **Both wires are then incident on a beamsplitter**, with free parameters $\theta$ and $\phi$.
   Here, we have the convention that the beamsplitter transmission amplitude is $t=\cos\theta$,
   and the reflection amplitude is
   $r=e^{i\phi}\sin\theta$. See :doc:`introduction/operations` for a full list of operation conventions.

4. **Finally, we measure the mean photon number** $\left\langle \hat{n}\right\rangle$ of the second wire, where

   .. math:: \hat{n} = \ad\a

   is the number operator, acting on the Fock basis number states, such that $\hat{n}\left|n\right\rangle = n\left|n\right\rangle$.

The aim of this tutorial is to optimize the beamsplitter parameters $(\theta, \phi)$ such
that the expected photon number of the second wire is **maximized**. Since the beamsplitter
is a passive optical element that preserves the total photon number, this to the output
state $\left|0,1\right\rangle$ — i.e., when the incident photon from the first wire has been
'redirected' to the second wire.

Exact calculation
~~~~~~~~~~~~~~~~~

To compare with later numerical results, we can first consider what happens analytically.
The initial state of the circuit is $\left|\psi_0\right\rangle=\left|1,0\right\rangle$, and the output state
of the system is of the form $\left|\psi\right\rangle = a\left|1, 0\right\rangle + b\left|0,1\right\rangle$, where
$|a|^2+|b|^2=1$. We may thus write the output state as a vector in this
computational basis, $\left|\psi\right\rangle = \begin{bmatrix}a & b\end{bmatrix}^T$.

The beamsplitter acts on this two-dimensional subspace as follows:

\begin{align}\left|\psi\right\rangle = B(\theta, \phi)\left|1, 0\right\rangle = \begin{bmatrix}
        \cos\theta & -e^{-i\phi}\sin\theta\\
        e^{i\phi}\sin\theta & \cos\theta
    \end{bmatrix}\begin{bmatrix} 1\\ 0\end{bmatrix} = \begin{bmatrix}
        \cos\theta\\
        e^{i\phi} \sin\theta
    \end{bmatrix}\end{align}

Furthermore, the mean photon number of the second wire is

\begin{align}\left\langle{\hat{n}_1}\right\rangle = \langle{\psi}\mid{\hat{n}_1}\mid{\psi}\rangle = |e^{i\phi} \sin\theta|^2
    \langle{0,1}\mid{\hat{n}_1}\mid{0,1}\rangle = \sin^2 \theta.\end{align}

Therefore, we can see that:

1. $0\leq \left\langle \hat{n}_1\right\rangle\leq 1$: the output of the quantum circuit is
   bound between 0 and 1;

2. $\frac{\partial}{\partial \phi} \left\langle \hat{n}_1\right\rangle=0$: the output of the
   quantum circuit is independent of the beamsplitter phase $\phi$;

3. The output of the quantum circuit above is maximised when $\theta=(2m+1)\pi/2$
   for $m\in\mathbb{Z}_0$.

Loading the plugin device
-------------------------

While PennyLane provides a basic qubit simulator (``'default.qubit'``) and a basic CV
Gaussian simulator (``'default.gaussian'``), the true power of PennyLane comes from its
`plugin ecosystem <https://pennylane.ai/plugins.html>`_, allowing quantum computations
to be run on a variety of quantum simulator and hardware devices.

For this circuit, we will be using the ``'strawberryfields.fock'`` device to construct
a QNode. This allows the underlying quantum computation to be performed using the
`Strawberry Fields <https://strawberryfields.readthedocs.io>`_ Fock backend.

As usual, we begin by importing PennyLane and the wrapped version of NumPy provided by PennyLane:

```python
import pennylane as qml
from pennylane import numpy as np
```

Next, we create a device to run the quantum node. This is easy in PennyLane; as soon as
the PennyLane-SF plugin is installed, the ``'strawberryfields.fock'`` device can be loaded
— no additional commands or library imports required.

```python
dev_fock = qml.device("strawberryfields.fock", wires=2, cutoff_dim=2)
```

Compared to the default devices provided with PennyLane, the ``'strawberryfields.fock'``
device requires the additional keyword argument:

* ``cutoff_dim``: the Fock space truncation used to perform the quantum simulation

Note

Devices provided by external plugins may require additional arguments and keyword arguments
    — consult the plugin documentation for more details.

Constructing the QNode
----------------------

Now that we have initialized the device, we can construct our quantum node. Like
the other tutorials, we use the :mod:`~.pennylane.qnode` decorator
to convert our quantum function (encoded by the circuit above) into a quantum node
running on Strawberry Fields.

```python
@qml.qnode(dev_fock, diff_method="parameter-shift")
def photon_redirection(params):
    qml.FockState(1, wires=0)
    qml.Beamsplitter(params[0], params[1], wires=[0, 1])
    return qml.expval(qml.NumberOperator(1))
```

The ``'strawberryfields.fock'`` device supports all CV objects provided by PennyLane;
see `CV operations <intro_ref_ops_cv>`.

Optimization
------------

Let's now use one of the built-in PennyLane optimizers in order to
carry out photon redirection. Since we wish to maximize the mean photon number of
the second wire, we can define our cost function to minimize the *negative* of the circuit output.

```python
def cost(params):
    return -photon_redirection(params)
```

To begin our optimization, let's choose the following small initial values of
$\theta$ and $\phi$:

```python
init_params = np.array([0.01, 0.01])
print(cost(init_params))
```

Here, we choose the values of $\theta$ and $\phi$ to be very close to zero;
this results in $B(\theta,\phi)\approx I$, and the output of the quantum
circuit will be very close to $\left|1, 0\right\rangle$ — i.e., the circuit leaves the photon in the first mode.

Why don't we choose $\theta=0$ and $\phi=0$?

At this point in the parameter space, $\left\langle \hat{n}_1\right\rangle = 0$, and
$\frac{d}{d\theta}\left\langle{\hat{n}_1}\right\rangle|_{\theta=0}=2\sin\theta\cos\theta|_{\theta=0}=0$.
Since the gradient is zero at those initial parameter values, the optimization
algorithm would never descend from the maximum.

This can also be verified directly using PennyLane:

```python
dphoton_redirection = qml.grad(photon_redirection, argnum=0)
print(dphoton_redirection([0.0, 0.0]))
```

Now, let's use the :class:`~.pennylane.GradientDescentOptimizer`, and update the circuit
parameters over 100 optimization steps.

```python
# initialise the optimizer
opt = qml.GradientDescentOptimizer(stepsize=0.4)

# set the number of steps
steps = 100
# set the initial parameter values
params = init_params

for i in range(steps):
    # update the circuit parameters
    params = opt.step(cost, params)

    if (i + 1) % 5 == 0:
        print("Cost after step {:5d}: {: .7f}".format(i + 1, cost(params)))

print("Optimized rotation angles: {}".format(params))
```

Comparing this to the `exact calculation <photon_redirection_calc>` above,
this is close to the optimum value of $\theta=\pi/2$, while the value of
$\phi$ has not changed — consistent with the fact that $\left\langle \hat{n}_1\right\rangle$
is independent of $\phi$.

Hybrid computation
------------------

To really highlight the capabilities of PennyLane, let's now combine the qubit-rotation QNode
from the `qubit rotation tutorial <qubit_rotation>` with the CV photon-redirection
QNode from above, as well as some classical processing, to produce a truly hybrid
computational model.

First, we define a computation consisting of three steps: two quantum nodes (the qubit rotation
and photon redirection circuits, running on the ``'default.qubit'`` and
``'strawberryfields.fock'`` devices, respectively), along with a classical function, that simply
returns the squared difference of its two inputs using NumPy:

```python
# create the devices
dev_qubit = qml.device("default.qubit", wires=1)
dev_fock = qml.device("strawberryfields.fock", wires=2, cutoff_dim=10)

@qml.qnode(dev_qubit)
def qubit_rotation(phi1, phi2):
    """Qubit rotation QNode"""
    qml.RX(phi1, wires=0)
    qml.RY(phi2, wires=0)
    return qml.expval(qml.PauliZ(0))

@qml.qnode(dev_fock, diff_method="parameter-shift")
def photon_redirection(params):
    """The photon redirection QNode"""
    qml.FockState(1, wires=0)
    qml.Beamsplitter(params[0], params[1], wires=[0, 1])
    return qml.expval(qml.NumberOperator(1))

def squared_difference(x, y):
    """Classical node to compute the squared
    difference between two inputs"""
    return np.abs(x - y) ** 2
```

Now, we can define an objective function associated with the optimization, linking together
our three subcomponents. Here, we wish to
perform the following hybrid quantum-classical optimization:

.. figure:: ../demonstrations/plugins_hybrid/hybrid_graph.png
    :align: center
    :width: 70%
    :target: javascript:void(0);

1. The qubit-rotation circuit will contain fixed rotation angles $\phi_1$ and $\phi_2$.

2. The photon-redirection circuit will contain two free parameters, the beamsplitter angles
   $\theta$ and $\phi$, which are to be optimized.

3. The outputs of both QNodes will then be fed into the classical node, returning the
   squared difference of the two quantum functions.

4. Finally, the optimizer will calculate the gradient of the entire computation with
   respect to the free parameters $\theta$ and $\phi$, and update their values.

In essence, we are optimizing the photon-redirection circuit to return the **same expectation value**
as the qubit-rotation circuit, even though they are two completely independent quantum systems.

We can translate this computational graph to the following function, which combines the three
nodes into a single hybrid computation. Below, we choose default values
$\phi_1=0.5$, $\phi_2=0.1$:

```python
def cost(params, phi1=0.5, phi2=0.1):
    """Returns the squared difference between
    the photon-redirection and qubit-rotation QNodes, for
    fixed values of the qubit rotation angles phi1 and phi2"""
    qubit_result = qubit_rotation(phi1, phi2)
    photon_result = photon_redirection(params)
    return squared_difference(qubit_result, photon_result)
```

Now, we use the built-in :class:`~.pennylane.GradientDescentOptimizer` to perform the optimization
for 100 steps. As before, we choose initial beamsplitter parameters of
$\theta=0.01$, $\phi=0.01$.

```python
# initialise the optimizer
opt = qml.GradientDescentOptimizer(stepsize=0.4)

# set the number of steps
steps = 100
# set the initial parameter values
params = np.array([0.01, 0.01])

for i in range(steps):
    # update the circuit parameters
    params = opt.step(cost, params)

    if (i + 1) % 5 == 0:
        print("Cost after step {:5d}: {: .7f}".format(i + 1, cost(params)))

print("Optimized rotation angles: {}".format(params))
```

Substituting this into the photon redirection QNode shows that it now produces
the same output as the qubit rotation QNode:

```python
result = [1.20671364, 0.01]
print(photon_redirection(result))
print(qubit_rotation(0.5, 0.1))
```

This is just a simple example of the kind of hybrid computation that can be carried
out in PennyLane. Quantum nodes (bound to different devices) and classical
functions can be combined in many different and interesting ways.


 Score de densité médicale

Kenza Elass et Rebecca Clain

```python
import pandas as pd
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn import preprocessing
sns.set_style('darkgrid')
pd.set_option('display.max_columns', None)
import ipywidgets as widgets
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
from IPython.display import display
import warnings
warnings.filterwarnings("ignore")
```

```python
df = pd.read_csv('analyse_df.csv', sep = ';', low_memory = False)

df.columns = ['code commune établissement', 'ambulance', 'analyse médicale', 'autre', 'autre spécialiste',
              'chirurgien','dentiste', 'generaliste', 'hopital', 'infirmiers', 'organe', 'radiologiste',
              'réeducateur podologue', 'code géographique', 'libellé géographique','nombre ménage fiscaux',
              'nombre personne ménage fiscaux', 'médiane niveau de vie', 'part ménages fiscaux imposés',
              'taux de pauvreté ensemble', 'taux de pauvreté moins de 30 ans', 'taux de pauvreté 30 à 39 ans',
              'taux de pauvreté 40 à 49 ans', 'taux de pauvreté 50 à 59 ans', 'Taux de pauvreté 60 à 74 ans',
              'taux de pauvreté 75 ans ou plus','taux de pauvreté propriétaires','taux de pauvreté locataires',
              'part des revenus activités','dont part des salaires et traitement hors chomage',
              'dont part des indemnites de chomage', 'dont part des revenus des activites non salaries',
              'part des pensions retraites et rentes','part des revenus du patrimoine et autres revenus',
              'part ensemble prestations sociales','dont part prestations familiales', 'dont part des minima sociaux',
              'dont part des prestations logement','part des impots','1er décile niveau de vie',
              '9e décile niveau de vie','rapport interdecile 9e/1er','merge_']
```

```python
def missing_values_table(df) :

        mis_val = df.isnull().sum()
        mis_val_percent = 100 * df.isnull().sum() / len(df)
        mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
        mis_val_table_ren_columns = mis_val_table.rename(
        columns = {0 : 'Missing Values', 1 : '% of Total Values'})
        mis_val_table_ren_columns = mis_val_table_ren_columns[
            mis_val_table_ren_columns.iloc[:,1] != 0].sort_values('% of Total Values', ascending = False).round(1)
        print ("Your selected dataframe has " + str(df.shape[1]) + " columns.\n"
            "There are " + str(mis_val_table_ren_columns.shape[0]) + " columns that have missing values.")

        return mis_val_table_ren_columns

missing_values_table(df)
```

    Your selected dataframe has 43 columns.
    There are 30 columns that have missing values.

      Missing Values
      % of Total Values

      taux de pauvreté 75 ans ou plus
      35540
      98.9

      taux de pauvreté moins de 30 ans
      35118
      97.8

      Taux de pauvreté 60 à 74 ans
      35068
      97.6

      taux de pauvreté 50 à 59 ans
      34623
      96.4

      taux de pauvreté 30 à 39 ans
      34480
      96.0

      taux de pauvreté 40 à 49 ans
      34047
      94.8

      taux de pauvreté propriétaires
      33769
      94.0

      taux de pauvreté locataires
      32722
      91.1

      taux de pauvreté ensemble
      31529
      87.8

      dont part des revenus des activites non salaries
      30665
      85.4

      dont part des minima sociaux
      30665
      85.4

      dont part des prestations logement
      30665
      85.4

      part des impots
      30665
      85.4

      1er décile niveau de vie
      30665
      85.4

      9e décile niveau de vie
      30665
      85.4

      dont part prestations familiales
      30665
      85.4

      part ensemble prestations sociales
      30665
      85.4

      part des revenus du patrimoine et autres revenus
      30665
      85.4

      part des pensions retraites et rentes
      30665
      85.4

      rapport interdecile 9e/1er
      30665
      85.4

      dont part des indemnites de chomage
      30665
      85.4

      dont part des salaires et traitement hors chomage
      30665
      85.4

      part des revenus activités
      30665
      85.4

      part ménages fiscaux imposés
      30661
      85.4

      code commune établissement
      15444
      43.0

      médiane niveau de vie
      3670
      10.2

      nombre personne ménage fiscaux
      3670
      10.2

      nombre ménage fiscaux
      3670
      10.2

      code géographique
      74
      0.2

      libellé géographique
      74
      0.2

# Construction d'un score de densité médicale

```python
df_dept = (df.dropna(subset = ['code commune établissement','nombre personne ménage fiscaux'])
             .assign(departement = lambda df : df['code commune établissement'].str[:2])
             .groupby('departement')
             [['nombre personne ménage fiscaux','ambulance', 'analyse médicale', 'autre',
             'autre spécialiste', 'chirurgien', 'dentiste', 'generaliste', 'hopital',
             'infirmiers', 'organe', 'radiologiste', 'réeducateur podologue']]
             .sum())
df_dept.head()
```

      nombre personne ménage fiscaux
      ambulance
      analyse médicale
      autre
      autre spécialiste
      chirurgien
      dentiste
      generaliste
      hopital
      infirmiers
      organe
      radiologiste
      réeducateur podologue

      departement

      01
      557566.0
      82.0
      39.0
      718.0
      203.0
      39.0
      368.0
      597.0
      138.0
      1070.0
      1.0
      38.0
      959.0

      02
      412914.0
      65.0
      36.0
      238.0
      263.0
      25.0
      261.0
      567.0
      119.0
      986.0
      5.0
      59.0
      562.0

      03
      300824.5
      59.0
      25.0
      307.0
      157.0
      35.0
      192.0
      392.0
      59.0
      774.0
      2.0
      30.0
      525.0

      04
      148467.5
      23.0
      18.0
      279.0
      118.0
      15.0
      134.0
      342.0
      48.0
      631.0
      0.0
      6.0
      419.0

      05
      127431.0
      33.0
      14.0
      235.0
      138.0
      21.0
      134.0
      379.0
      66.0
      483.0
      2.0
      25.0
      596.0

```python
soins = ['ambulance','analyse médicale','autre','autre spécialiste','chirurgien','dentiste',
         'generaliste','hopital','infirmiers','organe','radiologiste','réeducateur podologue']

for v in soins :
    df_dept[v + ' pour 100 000 habitants'] = 100000*df_dept[v]/df_dept['nombre personne ménage fiscaux']
```

Exemple de distribution des généralistes en France (estimation de la densité par méthode du noyau gaussien).

```python
plt.figure(figsize=(10,6))
sns.kdeplot(df_dept['generaliste pour 100 000 habitants'])
plt.axvline(df_dept['generaliste pour 100 000 habitants'].median(), color = 'red', linestyle = '--')
plt.show()
```

Nous appellerons densité médicale le nombre de médecins pour 100 000 habitants dans un département donné. Il existe une densité médicale pour chaque poste de soins (ambulance, dentiste, chirurgien, radiothérapiste, etc).

Pour simplifier, nous expliquerons la démarche en parlant uniquement de la densité de généraliste. La méthode reste la même pour les autres postes de soins.

Soit $x_{i}$ un vecteur ordonnant l'ensemble des densités de généralistes pour l'ensemble des départements $i$ (le rôle des poids $w_{i}$ sera présenté plus tard).

$x_{i} = [x_{min},...,x_{median},...,x_{max}] \:, w_{i} = [w_{min},...,w_{median},...,w_{max}]$

```python
df_dept['generaliste pour 100 000 habitants'].describe()
```

    count     97.000000
    mean     160.172290
    std       36.195403
    min      103.548581
    25%      135.185412
    50%      152.977729
    75%      182.096106
    max      297.415856
    Name: generaliste pour 100 000 habitants, dtype: float64

Nous avons donc : $x_{i} = [104,...,153,...,297] \:; w_{i} = [w_{min},...,w_{median},...,w_{max}]$

Nous fixons un poids égal à $w_{median} = 1$ pour la densité de généraliste médiane $x_{median}$.
Plus la densité de généraliste diminuera relativement à $x_{median}$, plus le poids $w$ diminuera.
A l'inverse, plus la densité de généraliste augmentera par rapport à $x_{median}$, plus le poids augmentera.

Pour cela, nous utilisons 2 boucles : la première pour remplir itérativement le vecteur les poids de $w_{median}$ jusqu'à $w_{min}$, la deuxième pour remplir itérativement le vecteur de poids de $w_{median}$ jusqu'à $w_{max}$.

```python
def RelativeWeight(variable) :

    x = np.sort(df_dept[variable].values)

    i = x.tolist().index(np.median(x))
    w = [0] * len(x)
    w[i] = 1

    i = x.tolist().index(np.median(x))
    while i >= 1 :
        w[i-1] = (w[i]*x[i-1])/x[i]
        i = i - 1

    j = x.tolist().index(np.median(x))
    while j <= len(w)-1 :
        try :
            w[j+1] = (w[j]*x[j+1])/x[j]
        except : IndexError
        pass
        j = j + 1

    d = pd.DataFrame(data = w, index = x, columns = ['Poids'])

    return d

df_dept['poids_generaliste'] = df_dept['generaliste pour 100 000 habitants'].map(
    RelativeWeight(variable = 'generaliste pour 100 000 habitants')["Poids"])

df_dept['poids_ambulance'] = df_dept['ambulance pour 100 000 habitants'].map(
    RelativeWeight(variable = 'ambulance pour 100 000 habitants')["Poids"])

df_dept['poids_chirurgien'] = df_dept['chirurgien pour 100 000 habitants'].map(
    RelativeWeight(variable = 'chirurgien pour 100 000 habitants')["Poids"])

df_dept['poids_dentiste'] = df_dept['dentiste pour 100 000 habitants'].map(
    RelativeWeight(variable = 'dentiste pour 100 000 habitants')["Poids"])

df_dept['poids_hopital'] = df_dept['hopital pour 100 000 habitants'].map(
    RelativeWeight(variable = 'hopital pour 100 000 habitants')["Poids"])

df_dept['poids_infirmiers'] = df_dept['infirmiers pour 100 000 habitants'].map(
    RelativeWeight(variable = 'infirmiers pour 100 000 habitants')["Poids"])

df_dept['poids_réeductateur_podologue'] = df_dept['réeducateur podologue pour 100 000 habitants'].map(
    RelativeWeight(variable = 'réeducateur podologue pour 100 000 habitants')["Poids"])

df_dept['poids_radiologiste'] = df_dept['radiologiste pour 100 000 habitants'].map(
    RelativeWeight(variable = 'radiologiste pour 100 000 habitants')["Poids"])

df_dept['poids_analyse_médicale'] = df_dept['analyse médicale pour 100 000 habitants'].map(
    RelativeWeight(variable = 'analyse médicale pour 100 000 habitants')["Poids"])

df_dept['poids_autre'] = df_dept['autre pour 100 000 habitants'].map(
    RelativeWeight(variable = 'autre pour 100 000 habitants')["Poids"])

df_dept['poids_autre_spécialiste'] = df_dept['autre spécialiste pour 100 000 habitants'].map(
    RelativeWeight(variable = 'autre spécialiste pour 100 000 habitants')["Poids"])

df_dept['poids_organe'] = df_dept['organe pour 100 000 habitants'].map(
    RelativeWeight(variable = 'organe pour 100 000 habitants')["Poids"].dropna())
```

Maintenant que les poids sont calculés, nous pouvons "ajuster" les densités, pour chaque département $i$, pour chaque poste de soins $j$.

\begin{equation}
x_{ij}^{adj} = w_{ij} \: x_{ij}
\end{equation}

```python
df_dept['ambulance_ajusté'] = df_dept['poids_ambulance'] * df_dept['ambulance pour 100 000 habitants']

df_dept['réeducateur_podologue_ajusté'] = df_dept['poids_réeductateur_podologue'] * df_dept['réeducateur podologue pour 100 000 habitants']

df_dept['analyse_médicale_ajusté'] = df_dept['poids_analyse_médicale'] * df_dept['analyse médicale pour 100 000 habitants']

df_dept['autre_ajusté'] = df_dept['poids_autre'] * df_dept['autre pour 100 000 habitants']

df_dept['autre_spécialiste_ajusté'] = df_dept['poids_autre_spécialiste'] * df_dept['autre spécialiste pour 100 000 habitants']

df_dept['chirurgien_ajusté'] = df_dept['poids_chirurgien'] * df_dept['chirurgien pour 100 000 habitants']

df_dept['dentiste_ajusté'] = df_dept['poids_dentiste'] * df_dept['dentiste pour 100 000 habitants']

df_dept['generaliste_ajusté'] = df_dept['poids_generaliste'] * df_dept['generaliste pour 100 000 habitants']

df_dept['hopital_ajusté'] = df_dept['poids_hopital'] * df_dept['hopital pour 100 000 habitants']

df_dept['infirmier_ajusté'] = df_dept['poids_infirmiers'] * df_dept['infirmiers pour 100 000 habitants']

df_dept['organe_ajusté'] = df_dept['poids_organe'] * df_dept['organe pour 100 000 habitants']

df_dept['radiologiste_ajusté'] = df_dept['poids_radiologiste'] * df_dept['radiologiste pour 100 000 habitants']
```

Nous avons ensuite décidé de sommer en colonne l'ensemble de ces nouvelles densités ajustées, de manière à obtenir comme une densité médicale globale ajustée, noté $y_{i}$

\begin{equation}
\displaystyle y_{i} = \sum_{j=1}^{m} x_{ij}^{adj} \\
\end{equation}

```python
f = []
for v in df_dept.columns :
    if v.endswith('ajusté') :
        f.append(v)
d = df_dept[f]
d['somme'] = d.sum(axis=1)
```

Enfin, nous appliquons une transformation min-max sur cette nouvelle variable, me permettant d'obtenir un score de densité médicale compris entre 0 et 1.

\begin{equation}
\displaystyle score_{i} = \frac{y_{i} - y_{min}}{y_{max} - y_{min}}
\end{equation}

```python
x = d['somme'].values.reshape(-1,1)
min_max_scaler = preprocessing.MinMaxScaler()
x_scaled = min_max_scaler.fit_transform(x)
d['score'] = x_scaled
```

Visualisons la distribution de ce score (tend vers 1 si la densité médicale est relativement forte, et vers 0 si la densité médicale est relativement faible).

```python
plt.figure(figsize=(11,8))
plt.subplot(2, 1, 1)
plt.hist(d['score'], bins = 20)
plt.subplot(2, 1, 2)
sns.kdeplot(d['score'], shade = True)
plt.show()
```

# Tableau de bord

```python
df_dept = df_dept.rename(columns = {'ambulance pour 100 000 habitants' : 'Ambulance',
                                    'analyse médicale pour 100 000 habitants' : 'Analyse médicale',
                                    'autre pour 100 000 habitants' : 'Autre',
                                    'autre spécialiste pour 100 000 habitants' : 'Autre spécialiste',
                                    'chirurgien pour 100 000 habitants' : 'Chirurgien',
                                    'dentiste pour 100 000 habitants' : 'Dentiste',
                                    'generaliste pour 100 000 habitants' : 'Généraliste',
                                    'hopital pour 100 000 habitants' : 'Hôpital',
                                    'infirmiers pour 100 000 habitants' : 'Infirmiers',
                                    'organe pour 100 000 habitants' : 'Organe',
                                    'radiologiste pour 100 000 habitants' : 'Radiologiste',
                                    'réeducateur podologue pour 100 000 habitants' : 'Réeducateur podologue'})

df_dept['Département'] = df_dept.index.values
df_dept['Score'] = x_scaled

a = widgets.ToggleButtons(
                          options = ['Ambulance',
                                     'Analyse médicale',
                                     'Autre',
                                     'Autre spécialiste',
                                     'Chirurgien',
                                     'Dentiste',
                                     'Généraliste',
                                     'Hôpital',
                                     'Infirmiers',
                                     'Organe',
                                     'Radiologiste',
                                     'Réeducateur podologue'],
                          description = 'Spécialité :',
                          disabled = False,
                          button_style = ''
                         )
b = widgets.Select(
                   options = df_dept['Département'].values.tolist(),
                   value = '01',
                   description = 'Département :',
                   disabled = False
                  )

display(a,b)

button = widgets.Button(description= "Voir les résultats")
display(button)

output = widgets.Output()

@output.capture()
def on_button_clicked(z):

    print('Dans le département n°', b.value,', la densité en', a.value.lower(),
    'est de', int(np.round(df_dept.loc[df_dept['Département'] == b.value, a.value].item(),0)),
    'pour 100 000 habitants. \nLe score de densité médicale de ce département est égal à',
    np.round(df_dept.loc[df_dept['Département'] == b.value, "Score"].item(),2))

button.on_click(on_button_clicked)
display(output)
```

    ToggleButtons(description='Spécialité :', options=('Ambulance', 'Analyse médicale', 'Autre', 'Autre spécialist…

    Select(description='Département :', options=('01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11',…

    Button(description='Voir les résultats', style=ButtonStyle())

    Output()