```python
#we may need some code in the ../python directory and/or matplotlib styles
import sys
import os
sys.path.append('../python/')

#set up matplotlib
os.environ['MPLCONFIGDIR'] = '../mplstyles'
print(os.environ['MPLCONFIGDIR'])
import matplotlib as mpl
from matplotlib import pyplot as plt
#got smarter about the mpl config: see mplstyles/ directory
plt.style.use('standard')
print(mpl.__version__)
print(mpl.get_configdir())

#fonts
# Set the font dictionaries (for plot title and axis titles)
title_font = {'fontname':'Arial', 'size':'16', 'color':'black', 'weight':'normal',
              'verticalalignment':'bottom'} # Bottom vertical alignment for more space
axis_font = {'fontname':'Arial', 'size':'32'}
legend_font = {'fontname':'Arial', 'size':'22'}

#fonts global settings
mpl.rc('font',family=legend_font['fontname'])

#set up numpy
import numpy as np
```

    ../mplstyles
    3.0.3
    /home/phys/villaa/analysis/misc/nrFano_paper2019/mplstyles

# ER/NR Band Calculation

In a previous notebook `QEr_2D_joint.ipynb` we calculated the correct two-dimensional probability distribution for energy recoils of arbitrary average ionization yield, given the resolutions ($\sigma_H$,$\sigma_I$, and $\sigma_N$) as functions of the true recoil energy. The variance on the number of e/h pairs created, $\sigma_N$ included the effective Fano factor, $F$.

This was all done starting from the fundamental variables $\delta H$, $\delta I$, and N (see `QEr_2D_joint.ipynb` for all variable definitions). The final result was the probability distribution in the Q,$\tilde{E}_r$ plane of measured yield and recoil energy, given a true recoil distribution $P(E_r)$. $P(E_r)$ was taken to be:

\begin{equation}
P(E_r) = \frac{1}{\alpha}e^{-\alpha E_r},
\end{equation}

where $\alpha$ is the decay constant and we take to be equal to about 1/100 keV$^{-1}$ for nuclear recoils.

In this note we will use this model to construct the variance of constant-$\tilde{E}_r$ slices of this distribution. The motivation for this calculation is that these variances are often the ones measured in experiments like Edelweiss [REF] and SuperCDMS.

## Ionization Yield Distributions at Fixed $\tilde{E}_r$

First, we want to construct bands for electron recoils with the parameters $F^{\prime}$=0 (Fano factor) and $\alpha^{\prime}$ = 1/100000 keV$^{-1}$ (essentially flat true recoil distribution).

**NOTE: The value of $F^{\prime}$ is choosen as such because the measured resolutions are done with electron recoils and implicitly include the true Fano factor. Although this Fano factor (typically in germanium of order 0.13 with large variability in the measurements [REF]) is included note that it is not included correctly because it is not separated from the variance of $\delta I$. Nevertheless, since the Fano contribution for electron recoils is small compared to the ionization resolutions in most published work, it is safely parameterized inside the $\delta I$ resolution for electron recoils.**

Next we will construct a nuclear recoil band with the parameters $F$=15 (this appears to be a plausible lower limit for measurements above 10 keV, see [REF] Dougherty, and the notebook constructing the Fano factor from that) and $\alpha$=1/100 keV$^{-1}$.

We are constructing the distribution that results from the following:

\begin{equation}
P(Q,\tilde{E}_r=E) = \int_0^{\infty} dE_r P(Q,\tilde{E}_r=E|E_r)P(E_r)
\end{equation}

the following two cells set all the parameters and get the correct resolutions. I am using detector GGA1 here to compare with because in Fig. 5 of the Edelweiss paper [REF] those bands are actually plotted and we have digitized versions of the 90% (1.645$\sigma$) containment bands.

```python
#constants
V=4.0 #volts
eps = 3.0/1000 #keV per pair, I usually use 3.3 for the numerator, but Edw. uses 3.
FWHM_to_SIG = 1 / (2*np.sqrt(2*np.log(2)))

#yield models
a=0.16
b=0.18
Qbar = lambda Er: a*Er**b
Qer = lambda Er: 1
```

```python
#getting the resolutions
import EdwRes as er

#these happen to be the parameters for GGA1 detector (Edelweiss)
aH=0.02
fh2 = er.get_heatRes_func(1.3, 3.5,aH*FWHM_to_SIG)
sigH = lambda x:fh2(x)

fi2 = er.get_ionRes_func(1.3, 1.3, 2.8)
sigI = lambda x:fi2(x)

#new resolution functions
Ehee = lambda Er: ((1+(V/(1000*eps))*Qbar(Er))*Er)/(1+(V/(1000*eps)))
EIee = lambda Er: Qbar(Er)*Er

sigH_NR = lambda Er: sigH(Ehee(Er))

sigI_NR = lambda Er: sigI(EIee(Er))

#check sigI @ 122keV
print('FWHM @ 122 keV: {}'.format(2.355*sigI(122)))
```

    FWHM @ 122 keV: 2.8002139755495987

In the next two cells, I set up the proability distributions for the afromentioned parameters with the functions developed as a result of the `QEr_2D.ipynb` notebook.

As a test energy I choose 10 keV because the distributions begin to look wide and clearly non-Gaussian there. At that energy we take a slice of the $P(Q,\tilde{E}_r)$ distribution, and calculate the integral so that we can properly normalize it. This is the ionization yield distribution that would be observed at a single measured recoil energy, $\tilde{E}_r$.

```python
import prob_dist as pd

#set up ER distribution
Fp = 0.0001
alphap=1/100000
Per = pd.QEr_v2_2D_fast(sigH,sigI,V,eps,Fp,Qer)
er_band = pd.expband_2D(Per,alphap,3)

#set up NR distribution
F = 15
alpha=1/100.0
Pnr = pd.QEr_v2_2D_fast(sigH_NR,sigI_NR,V,eps,F,Qbar)
nr_band = pd.expband_2D(Pnr,alpha,1.5)
```

```python
#test the dists at a certain energy
Etest=10

from scipy import integrate
ernorm = integrate.quad(er_band,0,4,args=(Etest,))[0]
nrnorm = integrate.quad(nr_band,0,4,args=(Etest,))[0]
print(ernorm)
print(nrnorm)

X=np.arange(0,1.5,0.01)
Qdist_er = lambda Q: (1/ernorm)*er_band(Q,Etest)
Qdist_nr = lambda Q: (1/nrnorm)*nr_band(Q,Etest)
Qerv = np.vectorize(Qdist_er)
Qnrv = np.vectorize(Qdist_nr)
Yer = Qerv(X)
Ynr = Qnrv(X)
```

    100038.55775424647
    88.38426954941066

The next two cells show the ER and NR distributions for the above parameters on a logarithmic and linear vertical scale. It can be seen on the logarithmic plots that the distributions are not Gaussian.

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

ax1.plot(X,Yer,color='orange',linestyle='-',label='{} keV ER band'.format(Etest))
ax1.plot(X,Ynr,color='steelblue',linestyle='-',label='{} keV NR band'.format(Etest))

ax1.axvline(Qbar(Etest), color='r', linestyle='--', lw=2, alpha=0.8,label='predicted centers')
ax1.axvline(1, color='r', linestyle='--', lw=2, alpha=0.8)

ymin = 1e-4
max1 = np.max(Yer)
max2 = np.max(Ynr)
ymax = 1.01*np.max(np.asarray([max1,max2]))

ax1.set_yscale('linear')
ax1.set_yscale('log')
ax1.set_xlim(0, 1.5)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'ionization yield',**axis_font)
ax1.set_ylabel('PDF',**axis_font)
ax1.grid(True)
#ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=4,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
#plt.savefig('figures/figure.png')
plt.show()
```

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

ax1.plot(X,Yer,color='orange',linestyle='-',label='{} keV ER band'.format(Etest))
ax1.plot(X,Ynr,color='steelblue',linestyle='-',label='{} keV NR band'.format(Etest))

ax1.axvline(Qbar(Etest), color='r', linestyle='--', lw=2, alpha=0.8,label='predicted centers')
ax1.axvline(1, color='r', linestyle='--', lw=2, alpha=0.8)

ymin = 1e-4
max1 = np.max(Yer)
max2 = np.max(Ynr)
ymax = 1.01*np.max(np.asarray([max1,max2]))

ax1.set_yscale('linear')
ax1.set_yscale('linear')
ax1.set_xlim(0, 1.5)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'ionization yield',**axis_font)
ax1.set_ylabel('PDF',**axis_font)
ax1.grid(True)
#ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=1,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
#plt.savefig('figures/figure.png')
plt.show()
```

## The 68.27% Symmetric Containment Region

Since these distributions are not quite Gaussian for all combinations of parameters, we must approximate the widths, $\sigma_Q(\tilde{E}_r)$. We define these to be the symmetric regions about the expected yield centers (note, it may be true that in some cases the distribution can be skewed so that neither the mean nor the mode are at the expected yield centers) that contain 68.27% of the distribution.

For this we can use the optimization libraries in `scipy.optimize`. We define a numerical function that will be zero when the size of the aformentioned region is supplied as an argument.

```python
import scipy.optimize as so

Qint_er = lambda a: integrate.quad(Qerv,Qer(Etest)-a,Qer(Etest)+a,limit=100)[0]
Qint_nr = lambda a: integrate.quad(Qnrv,Qbar(Etest)-a,Qbar(Etest)+a,limit=100)[0]

siger_zero = lambda a: Qint_er(a) - 0.6827 #one sigma
signr_zero = lambda a: Qint_nr(a) - 0.6827 #one sigma

siger_90 = lambda a: Qint_er(a) - 0.9 #90%
signr_90 = lambda a: Qint_nr(a) - 0.9 #90%

sigQer = so.brentq(siger_zero,0,1,rtol=0.001,maxiter=100)
print(sigQer)
sigQnr = so.brentq(signr_zero,0,1,rtol=0.001,maxiter=100)
print(sigQnr)

sigQer90 = so.brentq(siger_90,0,1,rtol=0.001,maxiter=100)
print('1.645xone sigma: {}; 90\%:{}'.format(1.645*sigQer,sigQer90))
sigQnr90 = so.brentq(signr_90,0,1,rtol=0.001,maxiter=100)
print('1.645xone sigma: {}; 90\%:{}'.format(1.645*sigQnr,sigQnr90))
```

    0.22478180172240245
    0.11474735445072594
    1.645xone sigma: 0.36976606383335203; 90\%:0.3698908536745943
    1.645xone sigma: 0.18875939807144418; 90\%:0.1840166980484885

```python
import time
start = time.time()
#root = pd.sigrootEdw(10,10,V,eps,(1/100),Qbar)
root = pd.sigrootEdw(Fp,7,V,eps,alphap,Qer)
end = time.time()
print('sigma: {}'.format(root))
print('{} s'.format(end - start))
```

    sigma: 0.3220219209327321
    9.04426646232605 s

```python
import fano_calc as fc

(nrsigma,nrE) = fc.calcQWidth(10,F,V,eps,alpha,Qbar,aH)
(ersigma,erE) = fc.calcQWidth(10,Fp,V,eps,alphap,Qer,aH)
```

In the Edelweiss paper [REF], a Gaussian approximation was used wherein the width in ionization yield could be written as:

\begin{equation}
\sigma_Q(\tilde{E}_r) \simeq \frac{1}{\tilde{E}_r} \sqrt{\left(1+\frac{V}{\epsilon}\bar{Q}\right)^2\sigma_I^2 + \left(1+\frac{V}{\epsilon}\right)^2\bar{Q}^2\sigma_H^2}.
\end{equation}

It is therefore useful to compare this function to the values obtained from the more fundamental derivation that we are seeking.

```python
#make functions for analytical bands
sigQer = lambda Etr: (1/Etr)*np.sqrt((1+(V/(1000*eps))*Qer(Etr))**2*sigI(Etr)**2 + (1+(V/(1000*eps)))**2*Qer(Etr)**2 \
                                     *sigH(Etr)**2)

sigQnr = lambda Etr: (1/Etr)*np.sqrt((1+(V/(1000*eps))*Qbar(Etr))**2*sigI_NR(Etr)**2 + (1+(V/(1000*eps)))**2*Qbar(Etr)**2 \
                                     *sigH_NR(Etr)**2)

print(sigQer(10))
print(sigQnr(10))
sigQerv = np.vectorize(sigQer)
sigQnrv = np.vectorize(sigQnr)
```

    0.2246462352359262
    0.10795032751310846

## Comparing Modeled and Edelweiss Published Bands

Since we can now calculate the 68.27% containments at a given recoil energy, we can find what they are for all recoil energy and compare to the bands published in the Edelweiss paper [REF]. We compare for detector GGA1, because that is the detector that the Edw. paper shows the bands from.

```python
import pandas as pd
band_data = pd.read_csv("data/edelweiss_bands_GGA1.csv")
print (band_data.head(10))
```

           x   Curve1   curve2   curve3   curve4
    0  1.667 -6.44581 -0.08957  1.49300 -0.64478
    1  1.802 -6.06069 -0.08789  1.44296 -0.62392
    2  1.804 -6.05415 -0.08786  1.46798 -0.62356
    3  1.938 -5.68168 -0.08613  1.41795 -0.60321
    4  2.075 -5.31531 -0.08430  1.39293 -0.58303
    5  2.212 -4.96139 -0.08240  1.36791 -0.56335
    6  2.883 -3.39678 -0.07177  1.11174 -0.47381
    7  3.158 -2.83384 -0.06656  1.10052 -0.44031
    8  3.295 -2.57110 -0.06372  1.06171 -0.42438
    9  4.518 -0.66571 -0.01776  0.75550 -0.30031

```python
# path 0: 68.75 keV inelastic scattering
# path 1: average NR line
# path 2: 13.26 keV inelastic
# path 3: ionization threshold
# path 4: lower nuclear recoil band
# path 5: upper nuclear recoil band
# path 6: upper and lower electron recoil band
import pandas as pd
band_data_svg = pd.read_csv("data/edelweiss_band_GGA1_allCurveData.txt", skiprows=1, header=None, delim_whitespace=True)
print (band_data_svg.head(5))
list(band_data_svg.columns.values)
```

            0         1        2         3        4         5        6        7   \
    0  69.4986  0.991839  1.02904  0.159922  14.2519  0.944224  2.10350  1.49896
    1  69.8626  0.987244  1.31515  0.167136  14.3933  0.936084  2.11548  1.48548
    2  70.2266  0.982648  1.60126  0.174349  14.5347  0.927944  2.12746  1.47200
    3  70.5907  0.978053  2.05730  0.181087  14.6761  0.919803  2.13943  1.45851
    4  70.9944  0.973552  2.45114  0.187750  14.8725  0.911724  2.15141  1.44503

            8         9        10       11        12       13
    0  11.0197 -0.002028  1.89725  1.50072   9.99176  1.50730
    1  11.3235  0.005082  1.92804  1.48860  10.39770  1.48881
    2  11.6309  0.012183  1.95883  1.47648  10.78450  1.47030
    3  11.9934  0.019148  1.98962  1.46436  11.35520  1.44694
    4  12.3254  0.026192  2.02041  1.45224  12.01670  1.42316

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

#X = np.arange(0.1,100,0.1)

erQ = Qer(erE)
nrQ = Qbar(nrE)

# plot the data-theifed bands
#ax1.plot(band_data.x[band_data.x>7.82], band_data.Curve1[band_data.x>7.82], 'b-',label='')
#ax1.plot(band_data.x, band_data.curve2, 'b-',label='')
#ax1.plot(band_data.x, band_data.curve3, 'r-',label='')
#ax1.plot(band_data.x, band_data.curve4, 'r-',label='')
ax1.plot(nrE,nrQ+1.645*nrsigma,'r-',linewidth=2,label='NR band model (F={:2.1f}; a$_H$={})'.format(F,aH))
ax1.plot(erE,erQ+1.645*ersigma,'b-',linewidth=2,label='ER band model (F={:2.1f}; a$_H$={})'.format(Fp,aH))
ax1.plot(nrE,nrQ-1.645*nrsigma,'r-',linewidth=2)
ax1.plot(erE,erQ-1.645*ersigma,'b-',linewidth=2)

ax1.plot(nrE,nrQ+1.645*sigQnrv(nrE),color='orange',linestyle='--',linewidth=3,label='NR model Edw. (a$_H$={})'.format(aH))
ax1.plot(erE,erQ+1.645*sigQerv(erE),color='steelblue',linestyle='--',linewidth=3,label='ER model Edw. (a$_H$={})'.format(aH))
ax1.plot(nrE,nrQ-1.645*sigQnrv(nrE),color='orange',linestyle='--',linewidth=3)
ax1.plot(erE,erQ-1.645*sigQerv(erE),color='steelblue',linestyle='--',linewidth=3)

#svg-extracted
# plot the svg? electron-recoil bands
ax1.plot(band_data_svg[12][band_data_svg[13]>1], band_data_svg[13][band_data_svg[13]>1], 'b:', linewidth=2, \
        label='Edw. paper ER')
ax1.plot(band_data_svg[12][band_data_svg[13]<1], band_data_svg[13][band_data_svg[13]<1], 'b:', linewidth=2,label='')

# nuclear recoil bands
ax1.plot(band_data_svg[8], band_data_svg[9], 'r:', linewidth=2, label="Edw. paper NR")
ax1.plot(band_data_svg[10], band_data_svg[11], 'r:', linewidth=2, label='')

#ax1.plot(X,ynr_muv(X),'r--',label='NR mu')
#ax1.plot(X,ynr_muv(X)+3*ynr_sigv(X),'r-',label='NR 3$\sigma$')
#ax1.plot(X,ynr_muv(X)-3*ynr_sigv(X),'r-',label=None)

#ax1.plot(X,yer_muv(X),color='orange',linestyle='--',label='ER mu')
#ax1.plot(X,yer_muv(X)+3*yer_sigv(X),color='orange',linestyle='-',label='ER 3$\sigma$')
#ax1.plot(X,yer_muv(X)-3*yer_sigv(X),color='orange',linestyle='-',label=None)

#ax1.axvline(t(t_test[idx]), color='k', linestyle='-', lw=2, alpha=0.8,label=None)

ymin = 0
ymax = 1.7

ax1.set_yscale('linear')
#ax1.set_yscale('linear')
ax1.set_xlim(0, 100)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'recoil energy [keV]',**axis_font)
ax1.set_ylabel('ionization yield',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
#ax1.legend(loc=1,prop={'size':22})
lgd = ax1.legend(bbox_to_anchor=(1.04,1),loc='upper left',borderaxespad=0,prop={'size':22})
#ax1.legend(bbox_to_anchor=(1.04,1),borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

#plt.tight_layout()
#plt.savefig('figures/GGA1_bandModel.png')
plt.savefig('figures/GGA1_bandModel.png', bbox_extra_artists=(lgd,), bbox_inches='tight')
plt.show()
```

# Matching Published and Calculated Widths

It is obvious from the plot above that, while our calculation using the fully correct distribution in the ionization-recoil plane matches very well with the Edw. paper model, it does not match with the bands extracted from Fig. 5 of the Edw. paper. I should plot these more carefully and look for discrepancies.

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

X=np.arange(0.1,160,0.1)

#ax1.plot(X,sigQnrv(X),color='r',linestyle="--",linewidth=2,label='single-scatter yield model (aH={:1.3})'.format(aH))
#ax1.plot(X,1.645*sigQerv(X),color='b',linestyle="--",linewidth=2,label='ER model Edw. (a$_H$={})'.format(aH))
ax1.plot(X,2.2*sigQerv(X),color='b',linestyle="--",linewidth=2,label='ER model Edw. (a$_H$={})'.format(aH))
ax1.plot(band_data_svg[12][band_data_svg[13]>1], band_data_svg[13][band_data_svg[13]>1]-1.0, 'b:', linewidth=2, \
        label='Edw. paper ER band plot')

ymin = 0.04
ymax = 0.2

ax1.set_yscale('linear')
#ax1.set_yscale('log')
ax1.set_xlim(0, 160)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'recoil energy [keV]',**axis_font)
ax1.set_ylabel('ionization yield width',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
#ax1.legend(loc=1,prop={'size':22})
#ax1.legend(loc='upper right', bbox_to_anchor=(1, 0.5),prop={'size':22})
lgd = ax1.legend(bbox_to_anchor=(1.04,1),loc="upper left",borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

#plt.tight_layout()
#plt.savefig('figures/figure.png')
#plt.savefig('figures/figure.png', bbox_extra_artists=(lgd,), bbox_inches='tight')
plt.show()
```

So, by adjusting $a_H$ to 0.02 **_and_** increasing the ER model do report 2.2$\sigma$ we get close. I can keep playing this game until I get the curve right, but why in the world would Edw. have plotted some arbitrary value like 2.2$\sigma$?

```python

```


Chapter 1: Restricted Hartree Fock (RHF) for Simple Atom (He and Be) using  Slater Type Orbital (STO)
    with  Double Zeta Basis in Python

Integral calculated by sympy

    SIMUC PROJECT

    By Jinze (Richard) Xue   /
    Source code

1. It will be better to understand if you have read Levine, Quantum Chemistry, 7th Edition, chapter 11 and 14.
There is an example calculation of Helium atom at page 412-414.

1. For well-organized and documented python file, please find in github: https://github.com/yueyericardo/simuc/tree/master/notebooks/pchem/hartree-fock/sto

1. This notebook is self-contained.
However, if you wanna try in other place, you should put `hf.py` file in the same directory where you wanna import it.

### Chapters
- Chapter 1: Restricted Hartree Fock (RHF) for Simple Atom (He and Be) using Slater Type Orbital (STO) with Double Zeta Basis in Python
- Chapter 2: Slater Type Orbital (STO) VS Gaussian Type Orbital (GTO) 【WIP】
  Interactive tool 【WIP】
- Chapter 3: Restricted Hartree Fock (RHF) for Simple diatomic molecule (H2 and HeH+) using Gaussian Type Orbital (GTO) with STO-3G Basis in Python  【Code is on [github](https://github.com/yueyericardo/simuc/tree/master/notebooks/pchem/hartree-fock/gto)】【Document WIP】
- Chapter 4: Restricted Hartree Fock (RHF) for Polyatomic Molecule using Simple and Powerful Package: PSI4【WIP】

### Overview
1. [Introduction](#1.-Introduction)
1. [Born–Oppenheimer approximation](#2.-Born–Oppenheimer-approximation)
2. [Hartree fock approximation](#3.-Hartree-fock-approximation)
3. [Why build matrix and how to solve secular equation?](#4.-Why-build-matrix-and-how-to-solve-secular-equation?)
4. [How to build matrix](#5.-How-to-build-matrix)
5. [Secular Equation](#6.-Secular-Equation)
6. [Total Energy](#7.-Total-Energy)
7. [Utils](#8.-Utils)
8. [Run Hartree Fock](#9.-Run-Hartree-Fock)
9. [Test](#10.-Test)
10. [Excise - Plot the charge density of orbitals](#11.-Excise---Plot-the-charge-density-of-orbitals)
10. [Limitations](#12.-Limitations)
11. [Reference](#13.-Reference)

---

### 1. Introduction
In this notebook, we will use Helium Atom as an example to explain Restricted Hartree Fock (RHF) method.
(Restricted means only for closed shell molecule, orbitals are either doubly occupied or empty)

Helium has two electrons on 1s orbital.

One-electron wavefunction of 1s orbital could be written as $^*$
$$\chi_{1 \mathrm{s}}^{\mathrm{STO}}=\left(\frac{\zeta^{3}}{\pi}\right)^{1 / 2} \exp (-\zeta r)$$

     [*]
    For simplicity, there is no angular part since it's s orbital.

    $\zeta$ is a constant related to the effective charge of the nucleus, the nuclear charge being partly shielded by electrons.

With double zeta, we are saying we will buil two $\chi_{1 \mathrm{s}}^{\mathrm{STO}}$ function. And the final 1s orbital of Helium will be a linear combination of these two STO.  $$\phi_1 = c_{11} \chi_1 + c_{21} \chi_2  \quad \text{(occupied orbital)}\;\; \tag {Eq1}$$ $$\phi_2 = c_{12} \chi_1 + c_{22} \chi_2 \quad \text{(unoccupied orbital)}$$

The goal of hartree fock is using Self-Consistent Field method (SCF) to optimize coefficients to get close to the real wavefunction.

For Helium, from reference [[1]](https://www.sciencedirect.com/science/article/pii/S0092640X74800161), two double zeta are 1.45363, 2.91093.
We could build these two STO with each zeta using sympy like below.

```python
import hf
import sympy as sp
import numpy as np
import scipy.linalg
from sympy import oo
from sympy import diff
import time
import matplotlib
from matplotlib import pyplot as plt
from IPython.display import Math
sp.init_printing()
%matplotlib inline
```

```python
r, r1, r2, zeta = sp.symbols("r, r1, r2, zeta")
n = sp.Symbol('n', integer=True)
```

```python
def STO(zeta, n, r=r):
    """
    Define a Slater Type Orbital function using sympy.

    INPUT:
    zeta: zeta for the STO.
    n: principle quantum number for the STO.
    """
    f = r ** (n - 1) * sp.exp(-zeta * r)
    # normalization
    N = sp.sqrt(1 / sp.integrate(4 * sp.pi * f * f * r * r, (r, 0, +oo)))
    return N * f
```

```python
f1s_1 = hf.STO(zeta=1.45363, n=1)
f1s_2 = hf.STO(zeta=2.91093, n=1)

display(Math('$\chi_1 :'))
display(f1s_1)
display(Math('$\chi_2 :'))
display(f1s_2)
```

 Note:  More detail about this will be covered at part 2, but I want to point out the main idea at beginning. So it could remind you what's our final goal, when you feel distracted.

By using Roothan equations below , which could be solved self-consistently for the orbital coefficient matrix **C** and orbital energy eigenvalues $\epsilon_i$ by iterations, we could finally
1. get close to the real wavefunction (using improved Coefficients for Eq1).
2. get close to correct Molecular orbital energies (using $\epsilon_i$), to get the real total energy

$${\mathbf  {F}}{\mathbf  {C}}={\mathbf  {S}}{\mathbf  {C}}{\mathbf  {\epsilon }}$$

     Note:
    F (Fock matrix), S (Overlap matrix) are inputs.

    S matrix is fixed, F matrix is changing every iteration because of the improved C.

    C (Coefficient matrix) and $\epsilon_i$ (eigenvalues) are results.

---

### 2. Born–Oppenheimer approximation
Using Born–Oppenheimer approximation, molecular Hamiltonian could be expressed as

$${\displaystyle H=H_{\text{e}}+T_{\text{n}}}$$

     Note:
    e (electron), n (nuclear)

Where

$${\displaystyle H_{\text{e}}=-\sum _{i}{{\frac {1}{2}}\nabla _{i}^{2}}-\sum _{i,\alpha}{\frac {Z_{\alpha}}{r_{i\alpha}}}+\sum _{i>j}{\frac {1}{r_{ij}}}+\sum _{\beta>\alpha}{\frac {Z_{\alpha}Z_{\beta}}{R_{\alpha \beta}}}\quad {\text{and}}\quad T_{\text{n}}=-\sum _{\alpha}{{\frac {1}{2M_{\alpha}}}\nabla _{\alpha}^{2}}}$$

     Note:
    i, j (electron), $\alpha$, $\beta$ (nuclear)

$H_e$:
1. sum of kinetic-energy operators for each electron
2. sum of nuclear–electronic Coulombic attraction terms
3. sum of electron-electron repulsion energy
4. sum of nuclear-nuclear repulsion energy ($V_{NN}$)

$T_n$:
1. sum of kinetic-energy operators for each nuclear

Classically, during the time of a cycle of electronic motion, the change in nuclear configuration is negligible. Thus, considering the nuclei as fixed, we omit the nuclear kinetic-energy terms $T_n$. So commonly when we say hartree fock energy, we are only talking about $H_e$ term.

$${\displaystyle H_{\text{e}}=-\sum _{i}{{\frac {1}{2}}\nabla _{i}^{2}}-\sum _{i,\alpha}{\frac {Z_{\alpha}}{r_{i\alpha}}}+\sum_{i} \sum_{j>i}{\frac {1}{r_{ij}}} + V_{NN}}$$

The first 3 terms together is purely electronic Hamiltonian.

---

### 3. Hartree fock approximation

Because of the inter-electronic repulsion term $\frac{1}{r_{i j}}$, the Schrödinger equation for a molecule wavefunction is not separable. So the true wave function cannot be written as the product of n one-electron functions.

The essence of hartree-fock approximation is to treat electron-electron repulsion in an average way, so this complicated many-electron problem could be solved as one-electron problem.

Then the molecular wavefunction could be written as a product of all one-electron wavefunctions.
The functions chosen to represent each electron is based on the hydrogen-like atomic wavefunction. ( Note: This is the reason why we use Slater type Orbital (STO), because it's transformed from exact wavefunction for an electron around a hydrogen atom)

$$
\Psi({r_1 r_2})=\phi_{1}\left(\boldsymbol{r}_{1}\right) \phi_{2}\left(\boldsymbol{r}_{2}\right)
$$

However this product does not satisfy antisymmetric requirements (which means if you swap electrons the sign of the wavefunction should invert). This problem can be overcome by taking a linear combination of both products:
$$
\begin{aligned} \Psi\left(\mathbf{r}_{1}, \mathbf{r}_{2}\right) &=\frac{1}{\sqrt{2}}\left\{\phi_{1}\left(\mathbf{r}_{1}\right) \phi_{2}\left(\mathbf{r}_{2}\right)-\phi_{1}\left(\mathbf{r}_{2}\right) \phi_{2}\left(\mathbf{r}_{1}\right)\right\} \\ &=\frac{1}{\sqrt{2}}\left|\begin{array}{ll}{\phi_{1}\left(\mathbf{r}_{1}\right)} & {\phi_{2}\left(\mathbf{r}_{1}\right)} \\ {\phi_{1}\left(\mathbf{r}_{2}\right)} & {\phi_{2}\left(\mathbf{r}_{2}\right)}\end{array}\right| \end{aligned}
$$

In this way, the Schrödinger equation would then be separated into n one-electron hydrogenlike equations.

Suppose there is an operator called Fock operator $\hat F$, the eigenvalue corresponding to $\hat F$ on a one-electron wavefunction is the energy related to this electron.

Each electron energy $\varepsilon_{i}$ will include
1. kinetic-energy for this electron
2. sum of coulombic attraction between this electron and all nuclears
3. the potential of this electron interacting with an averaged distribution of other electrons, (which is calculated by treating all of the other electrons within the molecule as a smooth distribution of negative charge, and this is the major simplification inherent in the Hartree–Fock method).
(What does this means? explained at below )

 Note:   If we take$\sum_{i}^{n} {\varepsilon_{i}}$, we will count each interelectronic repulsion twice, which needs to be subtracted when calculating total energy of molecule.

What's the meaing of the potential of this electron interacting with an averaged distribution of other electrons?
suppose we want to find the electron-electron repulsion potential ($\text{Vee}$) of electron 1 with electron 2.

$$\text{Vee}_{1 2}=\left\langle\phi (1)^*|\frac{1}{r_{1 2}} | \phi (1)\right\rangle$$
This is not solvable, because we don't know the location of electron (2).

And hartree fock simplify this to:
(recall that $\left|\phi(2)\right|^{2}$ is the probability density of electron (2)

$$\text{Vee}_{1 2}=\left\langle\phi (1)^*|\;\;\; \int \frac{\left|\phi(2)\right|^{2}}{r_{12}} d v_{2} \;\;\; | \phi (1)\right\rangle \tag{Eq2}$$

By using the probability density of electron (2) $\left|\phi(2)\right|^{2}$,

$$\text{infinitesimal charge density} * \text{infinitesimal volume} = \text{infinitesimal charge}$$

and integrate over all space, we could get the repulsion energy above.
It could be also rewritten as, which is more commonly used:

$$
\text{Vee}_{1 2}= \int \int \frac{\left|\phi(2)\right|^{2} \phi (1)^* \phi (1)}{r_{12}} d v_{2}d v_{1}
$$
$$
= \int \int \frac{\phi(2)^*\phi(2) \phi (1)^* \phi (1)}{r_{12}} d v_{2}d v_{1}
$$

Fock operator

The operator corresponding to one electron energy $\varepsilon_{i}$ is fock operator. (For the restricted case which assumes closed-shell orbitals and single- determinantal wavefunctions)

$$\hat{F}(i)=-\frac{1}{2} \nabla_{i}^{2}-\sum_{\alpha} \frac{Z_{\alpha}}{r_{1 \alpha}} + \sum_{j=1}^{n / 2}\left[2 \hat{J}_{j}(i)-\hat{K}_{j}(i)\right]$$

where:

- ${\displaystyle {\hat {F}}(i)}$ is the Fock operator for the i-th electron in the system,
- $-\frac{1}{2} \nabla_{i}^{2}-\sum_{\alpha} \frac{Z_{\alpha}}{r_{1 \alpha}} $ are kinetics energy and sum of nuclear-electron attraction respectively.
These two terms are often considered as the core terms, and refered as $\hat{H}^{\mathrm{core}}_i$

So Fock operator could be rewritten as
$$\hat{F}(i)=\hat{H}_{core}(i)+\sum_{j=1}^{n / 2}\left[2 \hat{J}_{j}(i)-\hat{K}_{j}(i)\right]$$

- ${\displaystyle n}$ is the number of electrons and ${\displaystyle {\frac {n}{2}}}$ is the number of occupied orbitals in the closed-shell system,
- ${\displaystyle {\hat {J}}_{j}(i)}$ is the Coulomb operator, defining the repulsive force between the j-th and i-th electrons in the system,  (explained at Eq2)
$$
\hat{J}_{j}(1) f(1) =f(1) \int\left|\phi_{j}(2)\right|^{2} \frac{1}{r_{12}} d v_{2}
$$

     Note:
    ${\displaystyle f(1)}$, ${\displaystyle f(2)}$ are the one-electron wavefunctions acted upon by the exchange operator as functions of the electron positions,
 and ${\displaystyle \phi _{j}(1)}$ and ${\displaystyle \phi _{j}(2)}$ are the one-electron wavefunction of the jth electron as functions of the positions of the electrons.

- ${\displaystyle {\hat {K}}_{j}(i)}$ is the exchange operator, defining the quantum effect produced by exchanging two electrons.

$$\hat{K}_{j}(1) f(1) =\phi_{j}(1) \int \frac{\phi_{j}^{*}(2) f(2)}{r_{12}} d v_{2}$$

The Coulomb operator is multiplied by two since there are two electrons in each occupied orbital. The exchange operator is not multiplied by two since it has a non-zero result only for electrons which have the same spin as the i-th electron.

Roothan equation

The eigenvalue corresponding to the fock operator is the molecular orbital energies, electrons will occupy start from the lowest level, since we are dealing closed shell molecule, each orbital will has 2 or 0 electrons.

The Roothaan equations are a representation of the Hartree–Fock equation in a non orthonormal basis set (molecular orbitals represented by linear combination of atomic orbitals), which could be solved in matrix way that computer is good at.

$${\mathbf  {F}}{\mathbf  {C}}={\mathbf  {S}}{\mathbf  {C}}{\mathbf  {\epsilon }}$$

     Note:
    F (Fock matrix), S (Overlap matrix) are inputs.

    S matrix is fixed, F matrix is changing every iteration because of the improved C.

    C (Coefficient matrix) and $\epsilon_i$ (eigenvalues) are results.

Why iterations?

Since the Fock operator depends on the orbitals used to construct the corresponding Fock matrix, the eigenfunctions of the Fock operator are in turn new orbitals, which can be used to construct a new Fock operator. In this way, the Hartree–Fock orbitals are optimized iteratively until the change in total electronic energy falls below a predefined threshold. In this way, a set of self-consistent one-electron orbitals is calculated. The Hartree–Fock electronic wave function is then the Slater determinant constructed from these orbitals. Following the basic postulates of quantum mechanics, the Hartree–Fock wave function can then be used to compute any desired chemical or physical property within the framework of the Hartree–Fock method and the approximations employed.

---

### 4. Why build matrix and how to solve secular equation?
Because in this way, computer could solve it efficiently.

     Why build matrix? Click to uncollapse
(This part is from Calculating Orbital Energies and Expansion Coefficients - Chemistry LibreTexts

Solving secular equation is actually calculating orbital energies and coefficients based on **variation principle**, which states that any approximate wavefunction must have a higher energy than the true wavefunction.
(This part is from [Calculating Orbital Energies and Expansion Coefficients - Chemistry LibreTexts](https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Book%3A_Symmetry_(Vallance)/20%3A_Calculating_Orbital_Energies_and_Expansion_Coefficients) .)

Let's ignore the coulomb $\hat J$ and exchange $\hat K$ operator in fock operator, what's left is one electron Hamiltonian core operator $\hat H_{core}$. So how to calculate eigenvalue of Hamiltonian core operator and it's corresponding wavefunction of Helium with double zeta basis?
(Fock operator could be used as the same way.)

$$
E=\frac{\langle\phi|\hat{H}| \phi\rangle}{\langle\phi | \phi\rangle}
$$
Where
$$\phi = c_{1} \chi_1 + c_{2} \chi_2 \quad \text{(unnormalized)} $$
Plug in $\phi$ and expand

$$
\begin{aligned} E &=\frac{\left\langle c_{1} \chi_{1}+c_{2} \chi_{2}|\hat{H}| c_{1} \chi_{1}+c_{2} \chi_{2}\right\rangle}{\left\langle c_{1} \chi_{1}+c_{2} \chi_{2} | c_{1} \chi_{1}+c_{2} \chi_{2}\right\rangle} \\ &=\frac{\left\langle c_{1} \chi_{1}|\hat{H}| c_{1} \chi_{1}\right\rangle+\left\langle c_{1} \chi_{1}|\hat{H}| c_{2} \chi_{2}\right\rangle+\left\langle c_{2} \chi_{2}|\hat{H}| c_{1} \chi_{1}\right\rangle+\left\langle c_{2} \chi_{2}|\hat{H}| c_{2} \chi_{2}\right\rangle}{\left\langle c_{1} \chi_{1}| c_{1}\chi_{1}\right\rangle+\left\langle c_{1} \chi_{1}| c_{2} \chi_{2}\right\rangle+\left\langle c_{2} \chi_{2} | c_{1} \chi_{1}\right\rangle+\left\langle c_{2} \chi_{2} | c_{2} \chi_{2}\right\rangle} \\ &=\frac{c_{1}^{2}\left\langle\chi_{1}|\hat{H}| \chi_{1}\right\rangle+ c_{1} c_{2}\left\langle\chi_{1}|\hat{H}| \chi_{2}\right\rangle+ c_{2} c_{1}\left\langle\chi_{2}|\hat{H}| \chi_{1}\right\rangle+ c_{2}^{2}\left\langle\chi_{2}|\hat{H}| \chi_{2}\right\rangle}{c_{1}^{2}\left\langle\chi_{1} | \chi_{1}\right\rangle+ c_{1} c_{2}\left\langle\chi_{1} | \chi_{2}\right\rangle+ c_{2} c_{1}\left\langle\chi_{2} | \chi_{1}\right\rangle+ c_{2}^{2}\left\langle  \chi_{2}|\chi_{2}\right\rangle} \end{aligned}
$$

If define
$$
H_{i j}=\left\langle\chi_{i}|\hat{H}| \chi_{j}\right\rangle
\quad and \quad
S_{i j}=\left\langle\chi_{i} | \chi_{j}\right\rangle
\tag {Eq3}
$$

     Note: This is where H matrix (same to Fock matrix) and S matrix comes from! We will come build these matrix at part 5.

and note that  $H_{ij}  =  H_{j}$  and  $S_{ij}  =  S_{ji}$ ,

$$
E=\frac{c_{1}^{2} H_{11}+2 c_{1} c_{2} H_{12}+c_{2}^{2} H_{22}}{c_{1}^{2} S_{11}+2 c_{1} c_{2} S_{12}+c_{2}^{2} S_{22}}
$$

$$
E\left(c_{1}^{2} S_{11}+2 c_{1} c_{2} S_{12}+c_{2}^{2} S_{22}\right)=c_{1}^{2} H_{11}+2 c_{1} c_{2} H_{12}+c_{2}^{2} H_{22}
$$

To minimize the energy with respect to  c1  and  c2,  we require

$$
\frac{\partial E}{\partial c_{1}}=0
\quad and \quad
\frac{\partial E}{\partial c_{2}}=0
$$

If we differentiate the above equation through separately by  c1  and  c2  and apply this condition, we will end up with two equations in the two unknowns  c1  and  c2 , which we can solve to determine the coefficients and the energy.

$$
\begin{array}{l}{E\left(2 c_{1} S_{11}+2 c_{2} S_{12}\right)=2 c_{1} H_{11}+2 c_{2} H_{12}} \\ {E\left(2 c_{1} S_{12}+2 c_{2} S_{22}\right)=2 c_{1} H_{12}+2 c_{2} H_{22}}\end{array}
$$

These are normally rewritten slightly, in the form
$$
\begin{array}{l}{c_{1}\left(H_{11}-E S_{11}\right)+c_{2}\left(H_{12}-E S_{12}\right)=0} \\ {c_{1}\left(H_{12}-E S_{12}\right)+c_{2}\left(H_{22}-E S_{22}\right)=0}\end{array} \tag{Eq4}
$$
Write this in matrix form gives
$$
\left(\begin{array}{cc}{H_{11}-E S_{11}} & {H_{12}-E S_{12}} \\ {H_{12}-E S_{12}} & {H_{22}-E S_{22}}\end{array}\right)\left(\begin{array}{c}{c_{1}} \\ {c_{2}}\end{array}\right)=\left(\begin{array}{l}{0} \\ {0}\end{array}\right)
$$
For the equations to have a solution, the determinant of the matrix must be equal to zero. Which means
$$
\left(H_{11}-E\right)\left(H_{22}-E\right)-\left(H_{12}-E S_{12}\right)^{2}=0
$$
Now, there is only one unkown variable E, solve the equation will commonly give us two E (eigenvalue).
And put this two E back to Eq4 will give us two set of (c1, c2), which corresponding to
$$\phi_1 = c_{11} \chi_1 + c_{21} \chi_2 \quad (\varepsilon_{1})$$
$$\phi_2 = c_{12} \chi_1 + c_{22} \chi_2 \quad (\varepsilon_{2})$$

Thanks for powerful scipy, secular equation could be solved simply by calling
`eigenvalue, C = scipy.linalg.eigh(H, S)`

```python
# H and S are calculated from next part
H = np.array([[-1.85073991, -1.88346692],
              [-1.88346692, -1.58510327]])
S = np.array([[1.        , 0.83752358],
              [0.83752358, 1.        ]])

e, Co = scipy.linalg.eigh(H, S)

print(e)
print(Co)
```

    [-1.97961968  1.03859384]
    [[-0.66167682  1.70635833]
     [-0.37818627 -1.79065634]]

Let's test if whether the eigenvalue and coefficient satisfy Eq4.
Note: `[-0.66167682, -0.37818627]` is the eigenvector corresponding to eigenvalue `-1.97961968`
`[1.70635833, -1.79065634]` is the eigenvector corresponding to eigenvalue `1.03859384`
The result below $e^{-17}$ and $e^{-16}$ are so close to 0, there is no much difference with 0.

```python
tmp1 = Co[0, 0] * (H[0, 0] - e[0] * S[0, 0]) + Co[1, 0] * (H[0, 1] - e[0] * S[0, 1])
tmp2 = Co[0, 1] * (H[0, 1] - e[1] * S[0, 1]) + Co[1, 1] * (H[1, 1] - e[1] * S[1, 1])
print(tmp1)
print(tmp2)
```

    2.7755575615628914e-17
    8.881784197001252e-16

### 5. How to build matrix

Note:   To avoid confusion, letters `r`, `s`, `t`, `u` are used to label matrix element and the basis functions $\chi$, and the letters `i`, `j` are used to label the MOs $\phi$.

Back to Eq3, $H_{rs}$ in H matrix and $S_{rs}$ in S matrix are defined as
$$
H_{r s}=\left\langle\chi_{r}|\hat{H}| \chi_{s}\right\rangle
\quad and \quad
S_{r s}=\left\langle\chi_{r} | \chi_{s}\right\rangle
$$

(a) Hamiltonian core matrix H

Where Hamiltonian core operator $\hat H$ is
$$\hat{H}^{\mathrm{core}}_i \equiv-\frac{1}{2} \nabla_{i}^{2}-\sum_{\alpha} \frac{Z_{\alpha}}{r_{i \alpha}}$$

$$H_{r s}=\left\langle\chi_{r}|\hat{H}| \chi_{s}\right\rangle
\quad $$

$$=\int_{0}^\infty \chi_r \hat{H} \chi_s \; 4\pi r^2dr$$

$$= \int_{0}^\infty \chi_r ((-\dfrac{1}{2}) \nabla^2 -\sum_{\alpha} \frac{Z_{\alpha}}{r_{i \alpha}})\chi_s \; 4\pi r^2 dr$$

Where $
{\text { Laplace operator: } \nabla^{2}} {=\frac{1}{r^{2}} \frac{\partial}{\partial r}\left(r^{2} \frac{\partial f}{\partial r}\right)=\frac{1}{r} \frac{\partial^{2}}{\partial r^{2}}(r f)}$
(For wavefunction which only has radial part.)

And since we are calculating atom, there is only one nuclear $\alpha$

$$\therefore H_{rs}= \int_{0}^\infty \chi_r ((-\dfrac{1}{2}) \dfrac{1}{r} \dfrac{\partial}{\partial r} \dfrac{\partial}{\partial r} r \chi_s - \dfrac{Z_{\alpha}}{r} \chi_s )4\pi r^2 dr$$

Let's write this in sympy.
For easy reading, the code below seperate equation above into
$ H_{rs}= \int_{0}^\infty \chi_r (T - V )4\pi r^2 dr$

```python
def H_int(fr, fs, Z):
    """
    Compute H_core integral between two STO functions.
    H_core = electron kinetics energy + electron nuclear potential energy

    INPUT:
    Z: Nuclear charge
    """
    T = - ((1 / 2) * (1 / r) * diff(diff(r * fs, r), r))
    V = - (Z / r) * fs
    return sp.integrate(fr * (T + V) * 4 * sp.pi * r * r, (r, 0, +oo))
```

`H_int` function calculate $H_{rs}$ element in H matrix. To build H matrix, we just need to go over `r` from (1, 2, 3 ... num_bfs), and go over `s` also from (1, 2, 3 ... num_bfs), where `num_bfs` is number of basis functions, we will see this more at below.

```python
def H_matrix(bfs, Z):
    """
    Compute the core hamiltonian matrix H.
    H_core = electron kinetics energy + electron nuclear potential energy

    INPUT:
        bfs: basis functions
        Z: nuclear charge
    OUTPUT:
        H: core hamiltonian matrix
    """
    num_bfs = len(bfs)
    H = np.zeros((num_bfs, num_bfs))

    for r in range(num_bfs):
        for s in range(num_bfs):
            H[r, s] = H_int(bfs[r], bfs[s], Z)

    return H
```

(b) Overlap matrix S

If you understand H matrix, S (Overlap) matrix will be very easy.

$$S_{rs} = \int_{0}^\infty \chi_r^* \chi_s  \; 4 \pi r^2dr$$

```python
def S_int(fr, fs):
    """
    Compute overlap integral between two STO functions.
    """
    return sp.integrate(fr * fs * 4 * sp.pi * r * r, (r, 0, +oo))
```

```python
def S_matrix(bfs):
    """
    Compute overlap matrix S.

    INPUT:
        fs: basis functions
    OUTPUT:
        S: Overlap matrix
    """
    num_bfs = len(bfs)
    S = np.zeros((num_bfs, num_bfs))

    for r in range(num_bfs):
        for s in range(num_bfs):
            S[r, s] = S_int(bfs[r], bfs[s])

    return S
```

(c) Fock matrix F

Fock matrix F = H matrix + G matrix

Fock matrix could be generated similarly like H, where Fock operator is
$$\hat{F}(i)=\hat{H}_{core}(i)+\sum_{j=1}^{n / 2}\left[2 \hat{J}_{j}(i)-\hat{K}_{j}(i)\right]$$

The second term is often denoted as $\hat G$.

$$\hat{F}(i)=\hat{H}_{core}(i)+\hat G$$

Because we already have **H matrix** above, so we only need to build **G matrix**, add them together will generate **Fock matrix**.

G matrix

The defination of $J$ (Coulomb operator) and $K$ (exchange operator) are. (1) (2) below simple means they are different electron.

$$
\begin{aligned} \hat{J}_{j}(1) f(1) &=f(1) \int\left|\phi_{j}(2)\right|^{2} \frac{1}{r_{12}} d v_{2} \\ \hat{K}_{j}(1) f(1) &=\phi_{j}(1) \int \frac{\phi_{j}^{*}(2) f(2)}{r_{12}} d v_{2} \end{aligned}
$$

     Note:
    ${\displaystyle f(1)}$, ${\displaystyle f(2)}$ are the one-electron wavefunctions acted upon by the exchange operator as functions of the electron positions,
 and ${\displaystyle \phi _{j}(1)}$ and ${\displaystyle \phi _{j}(2)}$ are the one-electron wavefunction of the jth electron as functions of the positions of the electrons.

Let's see the result first, we will walk through an example ($G_{12}$ of helium) below explain how to get here. (Basically, it's just expand $\phi_i$ into linear combination of the basis functions.)
$$
\left\langle\chi_{r}(1) | \hat{J}_{j}(1) \chi_{s}(1)\right\rangle=\sum_{t} \sum_{u} c_{t j}^{*} c_{u j} \iint \frac{\chi_{r}^{*}(1) \chi_{s}(1) \chi_{t}^{*}(2) \chi_{u}(2)}{r_{12}} d v_{1} d v_{2}
$$
$$
\left\langle\chi_{r}(1) | \hat{K}_{j}(1) \chi_{s}(1)\right\rangle=\sum_{t} \sum_{u} c_{t j}^{*} c_{u j} \iint \frac{\chi_{r}^{*}(1) \chi_{u}(1) \chi_{t}^{*}(2) \chi_{s}(2)}{r_{12}} d v_{1} d v_{2}
$$
If define

$$
(r s | t u) \equiv \iint \frac{\chi_{r}^{*}(1) \chi_{s}(1) \chi_{t}^{*}(2) \chi_{u}(2)}{r_{12}} d v_{1} d v_{2}
$$

It could be rewritten as
$$
\left\langle\chi_{r}(1) | \hat{J}_{j}(1) \chi_{s}(1)\right\rangle=\sum_{t=1}^{b} \sum_{u=1}^{b} c_{t j}^{*} c_{u j}(r s | t u)
$$

$$
\left\langle\chi_{r}(1) | \hat{K}_{j}(1) \chi_{s}(1)\right\rangle=\sum_{t=1}^{b} \sum_{u=1}^{b} c_{t j}^{*} c_{u j}(r u | t s)
$$
And final $G_{rs}$ will be
$$
G_{r s}=\sum_{t=1}^{b} \sum_{u=1}^{b} \sum_{j=1}^{n / 2} c_{t j}^{*} c_{u j}[2(r s | t u)-(r u | t s)]
$$

     Example:

Let's take  $G_{12}$ element in G matrix (Helium atom) as an example to see how this comes out. (click to uncollapse)

$$G_{r s}=\left\langle\chi_{r}|\hat{G}| \chi_{s}\right\rangle$$
$$G_{1 2}=\left\langle\chi_{1}|\hat{G}| \chi_{2}\right\rangle$$

$$G_{1 2}= \sum_{j=1}^{n / 2} \left\langle\chi_{1}|2 \hat{J}_{j}(i)-\hat{K}_{j}(i)| \chi_{2}\right\rangle$$

For helium atom, num of electron (n) is 2, there is only one 1s orbital.

$$G_{1 2}= \left\langle\chi_{1}|2 \hat{J}_{j}(i)-\hat{K}_{j}(i)| \chi_{2}\right\rangle$$
$$G_{1 2}= 2\left\langle\chi_{1}| \hat{J}_{j}(i)| \chi_{2}\right\rangle - \left\langle\chi_{1}|\hat{K}_{j}(i)| \chi_{2}\right\rangle$$

Let's do $\left\langle\chi_{1}| \hat{J}_{j}(i)| \chi_{2}\right\rangle$ first

$$\left\langle\chi_{1}(1)| \hat{J}_{1}| \chi_{2}(1)\right\rangle $$
$$= \left\langle\chi_{1}(1)| \;\;\; \int \frac{\left|\phi_1(2)\right|^{2}}{r_{12}} d v_{2} \;\;\;  | \chi_{2}(1)\right\rangle $$
$$
= \int \int \frac{\phi_1(2)^*\phi_1(2) \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
$$

$$
= \int \int \frac{[c_{11}\chi_{1}^*(2) + c_{21}\chi_{2}^*(2)][c_{11}\chi_{1}(2) + c_{21}\chi_{2}(2)] \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
$$

$$
= \int \int \frac{c_{11}\chi_{1}^*(2)c_{11}\chi_{1}(2) \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
+ \int \int \frac{c_{11}\chi_{1}^*(2)c_{21}\chi_{2}(2) \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
$$
$$
+ \int \int \frac{c_{21}\chi_{2}^*(2)c_{11}\chi_{1}(2) \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
+ \int \int \frac{c_{21}\chi_{2}^*(2)c_{21}\chi_{2}(2) \chi (1)^* \chi (1)}{r_{12}} d v_{2}d v_{1}
$$

Use
$
(r s | t u) \equiv \iint \frac{\chi_{r}^{*}(1) \chi_{s}(1) \chi_{t}^{*}(2) \chi_{u}(2)}{r_{12}} d v_{1} d v_{2}
$ to simplify the representation.

$$
\left\langle\chi_{1}| \hat{J}_{j}(i)| \chi_{2}\right\rangle
= c_{11}c_{11}(1 2 | 1 1) + c_{11}c_{21}(1 2 | 1 2) + c_{21}c_{11}(1 2 | 2 1) + c_{21}c_{21}(1 2 | 2 2)
$$
$$
=\sum_{t=1}^{2} \sum_{u=1}^{2} \sum_{j=1}^{2 / 2} c_{t j}^{*} c_{u j} (1 \;2 | t \;u)
$$

Similar $\left\langle\chi_{1}| \hat{K}_{j}(i)| \chi_{2}\right\rangle$ could also be inferred,
$$
\left\langle\chi_{1}| \hat{K}_{j}(i)| \chi_{2}\right\rangle
= c_{11}c_{11}(1 1 | 1 2) + c_{11}c_{21}(1 2 | 1 2) + c_{21}c_{11}(1 1 | 2 2) + c_{21}c_{21}(1 2 | 2 2)
$$
$$
=\sum_{t=1}^{2} \sum_{u=1}^{2} \sum_{j=1}^{2 / 2} c_{t j}^{*} c_{u j} (1 \;u | t \;2)
$$

So $G_{1 2}$ is
$$G_{1 2}= \left\langle\chi_{1}|2 \hat{J}_{j}(i)-\hat{K}_{j}(i)| \chi_{2}\right\rangle$$
$$
=\sum_{t=1}^{2} \sum_{u=1}^{2} \sum_{j=1}^{2 / 2} c_{t j}^{*} c_{u j}[2(1 \;2 | t \;u)-(1\; u | t \;2)]
$$

Build G matrix in python

recall $G_{rs}$ is
$$
G_{r s}=\sum_{t=1}^{b} \sum_{u=1}^{b} \sum_{j=1}^{n / 2} c_{t j}^{*} c_{u j}[2(r s | t u)-(r u | t s)]
$$

If define $P_{rs}$ (density matrix) as
$$
P_{t u} \equiv 2 \sum_{j=1}^{n / 2} c_{t j}^{*} c_{u j}, \quad t=1,2, \ldots, b, \quad u=1,2, \ldots, b
$$

Then $G_{rs}$ could be simplified again as
$$
G_{r s}=\sum_{t=1}^{b} \sum_{u=1}^{b} P_{t u}\left[(r s | t u)-\frac{1}{2}(r u | t s)\right]
$$

If we built a matrix **R** which is 4 dimension, and save all the posible $(r s | t u)$, then we could build G matrix like below.

```python
def G_matrix(P, R):
    """
    Compute G matrix.
    G =  coulombic repulsion energy + exchange energy

    INPUT:
        P: density matrix
        R: electron repulsion matrix
    OUTPUT:
        G: repulsion matrix
    """
    num_bfs = P.shape[0]
    G = np.zeros((num_bfs, num_bfs))

    for r in range(num_bfs):
        for s in range(num_bfs):
            g = 0
            for t in range(num_bfs):
                for u in range(num_bfs):
                    int1 = R[r, s, t, u]
                    int2 = R[r, u, t, s]
                    g += P[t, u] * (int1 - 0.5 * int2)
            G[r, s] = g

    return G
```

P matrix

And also P (density) matrix
$$
P_{t u} \equiv 2 \sum_{j=1}^{n / 2} c_{t j}^{*} c_{u j}, \quad t=1,2, \ldots, b, \quad u=1,2, \ldots, b
$$

```python
def P_matrix(Co, N):
    """
    Compute density matrix P.

    INPUT:
        Co: coefficents matrix
        N: num of electrons
    OUTPUT:
        P: repulsion matrix
    """
    P = np.zeros([Co.shape[0], Co.shape[0]])

    for t in range(Co.shape[0]):
        for u in range(Co.shape[0]):
            for j in range(int(N/2)):
                P[t, u] += 2 * Co[t, j] * Co[u, j]
    return P
```

R matrix

R matrix (4 dimenstion), which is the most computation-expensive part in hartree-fock method. (In principle, there are a lot of elements in R matrix are equal by symmetry, but for code simplicity, we didn't implemented this.)

```python
def R_matrix(bfs):
    """
    Compute the electron repulsion integral matrix R.

    INPUT:
        fs: basis functions
    OUTPUT:
        R: repulsion matrix
    """
    start = time.time()
    num_bfs = len(bfs)
    R = np.zeros((num_bfs, num_bfs, num_bfs, num_bfs))

    for r in range(num_bfs):
        for s in range(num_bfs):
            for t in range(num_bfs):
                for u in range(num_bfs):
                    R[r, s, t, u] = R_int([bfs[r], bfs[s], bfs[t], bfs[u]])

    stop = time.time()
    print('time Repu: {:.1f} s'.format(stop-start))
    return R
```

Recall the element $R_{rstu}$ in R matrix is
$$
(r s | t u) \equiv \iint \frac{\chi_{r}^{*}(1) \chi_{s}(1) \chi_{t}^{*}(2) \chi_{u}(2)}{r_{12}} d v_{1} d v_{2}
$$

For 1s or 2s orbital which only has radial part, this could be calculated approximately in sympy. However, for the orbitals which has angular part, how to solve two-electron Repulsion integral of Slater Type Orbital (STO) is still slow and difficult. This is the main reason why Gaussian Type Orbital (GTO) is more frequently used, and we will try GTO in next chapter.

$$(rs|tu)  = \int_{0}^\infty \int_{0}^\infty \dfrac{\chi_r^*(1) \chi_s(1) \chi_t^*(2) \chi_u(2)}{r_{12}} \; 4 \pi r_1^2dr_1\; 4 \pi r_2^2dr_2  $$

$$(rs|tu) = \int_{0}^\infty \chi_r^*(1) \chi_s(1)  \; 4 \pi r_1^2dr_1\int_{0}^\infty \frac{ \chi_t^*(2) \chi_u(2)}{r_{12}}\; 4 \pi r_2^2dr_2  $$

$r_{12}$ here is tricky to deal with, from problem 9.14 in quantum_chemistry by levine

$$(rs|tu) = \int_{0}^\infty \chi_r^*(1) \chi_s(1)  \; 4 \pi r_1^2dr_1\int_{0}^\infty \frac{ \chi_t^*(2) \chi_u(2)}{r_{>}}\; 4 \pi r_2^2dr_2  $$

     Note:
    $r_{>}$ is the larger one between $r1$ and $r2$

$$(rs|tu) = \int_{0}^\infty \chi_r^*(1) \chi_s(1)  \; 4 \pi r_1^2dr_1(\int_{0}^{r_1} \frac{ \chi_t^*(2) \chi_u(2)}{r_{1}}\; 4 \pi r_2^2dr_2 + \int_{r_1}^\infty \frac{ \chi_t^*(2) \chi_u(2)}{r_{2}}\; 4 \pi r_2^2dr_2)  $$

Let$ \; B= \int_{0}^{r_1} \frac{ \chi_t^*(2) \chi_u(2)}{r_{1}}\; 4 \pi r_2^2dr_2 + \int_{r_1}^\infty \frac{ \chi_t^*(2) \chi_u(2)}{r_{2}}\; 4 \pi r_2^2dr_2$

$$(rs|tu) = \int_{0}^\infty \chi_r^*(1) \chi_s(1) B \; 4 \pi  r_1^2 dr_1  $$

So the element $R_{rstu}$ in R matrix could be calculated by

```python
def R_int(four_bfs):
    """
    Compute electron-electron repulsion integral.

    INPUT:
    four_bfs: an array contain 4 basis functions
    """
    f1, f2, f3, f4 = four_bfs

    f1 = f1.subs(r, r1)
    f2 = f2.subs(r, r1)
    f3 = f3.subs(r, r2)
    f4 = f4.subs(r, r2)

    B = (1 / r1) * sp.integrate(f3 * f4 * 4 * sp.pi * r2 * r2, (r2, 0, r1)) + sp.integrate((1 / r2) * f3 * f4 * 4 * sp.pi * r2 * r2, (r2, r1, +oo))
    return sp.integrate(f1 * f2 * 4 * sp.pi * r1 * r1 * B, (r1, 0, +oo))
```

This is basically all the matrixes we will need to use, and we already solved the most difficult part of hartree fock!!

### 6. Secular Equation

**Fock matrix** is the sum of **H matrix** and **G matrix**.
F = H + G

Then, Roothan equation could be solved simply by calling
`eigenvalue, C = scipy.linalg.eigh(F, S)`
eigenvalues are MO orbital energies.

$${\mathbf  {F}}{\mathbf  {C}}={\mathbf  {S}}{\mathbf  {C}}{\mathbf  {\epsilon }}$$

     Note:
    F (Fock matrix), S (Overlap matrix) are inputs.

    S matrix is fixed, F matrix is changing every iteration because of the improved C.

    C (Coefficient matrix) and $\epsilon_i$ (eigenvalues) are results.

We could wrap `scipy.linalg.eigh` into a function

```python
def secular_eqn(F, S):
    """
    Slove secular equation, return the MO energies (eigenvalue) and improved coeffients (eigenvector)

    INPUT:
        F: fock matrix or h_core matrix
        S: overlap integral
    OUTPUT:
        ei: eigenvalue
        C: eigenvector
    """
    ei, C = scipy.linalg.eigh(F, S)
    return ei, C
```

### 7. Total Energy
Finally, hartree fock total energy is
$$
E_{\mathrm{HF}}=2 \sum_{i=1}^{n / 2} \varepsilon_{i}-\sum_{i=1}^{n / 2} \sum_{j=1}^{n / 2}\left(2 J_{i j}-K_{i j}\right)+V_{N N}
$$

     Note:
    i, j are electron (only r, s, t, u are used for matrix element)

Where every term is
1. Energy of all the electrons (in closed shell condition, every orbital is doubly-occupied)
2. Recall that when we build F operator and calculat electron energy, electron-electron repulsion is counted twice when you sum all the electrons energy. This is the remove the extra count.
3. Nuclear-Nuclear repulsion term, which is not included in F operator. (because we are calculating Atom, this term is always 0)

half sum of the all the electrons $({n / 2})$ could also be written as
$$
\sum_{i=1}^{n / 2} \varepsilon_{i}=\sum_{i=1}^{n / 2} H_{i i}^{\mathrm{core}}+\sum_{i=1}^{n / 2} \sum_{j=1}^{n / 2}\left(2 J_{i j}-K_{i j}\right)
$$

So $E_{\mathrm{HF}}$ could be simplifed as
$$
E_{\mathrm{HF}}=\sum_{i=1}^{n / 2} \varepsilon_{i}+\sum_{i=1}^{n / 2} H_{i i}^{\mathrm{core}}+V_{N N}
$$

     Note:
    i, j are electron (only r, s, t, u are used for matrix element)

    H here is not H matrix, which is $
H_{i i}^{\text {core }}=\left\langle\phi_{i}\left|\hat{H}^{\text {core }}\right| \phi_{i}\right\rangle
$

Expand $\phi_{i}$ into basis functions and simplify, we could calculate total energy using the matrix we have built before.
$$
E_{\mathrm{HF}}=\sum_{i=1}^{n / 2} \varepsilon_{i}+\frac{1}{2} \sum_{r=1}^{b} \sum_{s=1}^{b} P_{r s} H_{r s}^{\mathrm{core}}+V_{N N}
$$

```python
def energy_tot(e, N, P, H, Vnn=0):
    """
    Compute the total energy.

    INPUT:
    e: MO energies
    N: num of electrons
    P: density matrix
    H: h_core matrix
    Vnn: nuclear nuclear repulsion energy, for atom is 0
    """
    e_tot = 0

    for i in range(int(N/2)):
        e_tot += e[i].real

    e_tot = e_tot + 0.5 * (P * H).sum() + Vnn
    return e_tot
```

### 8. Utils

And also some utils function to print information and compare our result with reference

```python
def print_info(S, H, e, Co, P, hf_e, start, stop, delta_e=0, verbose=False):
    """
    Print information while doing SCF interations.
    """
    if(verbose):
        # overlap
        print('Overlap:')
        print(S)

        # hamiltonian
        print('Core hamiltonian:')
        print(H)

        # Co
        print('Coefficients:')
        print(Co)

        # density
        print('Density matrix:')
        print(P)

        # MOs
        print('MO energies:')
        message = ', '
        m_list = ['e{} = {:0.3f}'.format(i+1, x) for i, x in enumerate(e)]
        message = message.join(m_list)
        print(message)

    print('HF energy: {:0.5f} (hartree) = {:0.5f} (eV)'.format(hf_e, hf_e*27.211))
    if delta_e != 0:
        print('dE       : {:.2e}'.format(delta_e))
    print('time used: {:.1f} s'.format(stop-start))

def compare(cal, ref, tol=1.0e-4):
    """
    Compare calculated result with reference data.
    """
    delta = np.abs(ref - cal)
    if delta < tol:
        message = '\33[32m' + 'PASSED' + '\x1b[0m'
    else:
        message = '\033[91m' + 'FAILED' + '\033[0m'
    print('-' * 32, message, '-' * 33)
    print('cal: {:.7f}, ref: {:.7f}\n\n'.format(cal, ref))
```

### 9. Run Hartree Fock

Steps to run hartree fock

1. Initialization
  - Let Fock matrix = H_core matrix, without considering electron repulsion
  - Solve secular equation with H and S to get initial Co (means initial guessed molecular orbitals) and build inital P (density) matrix
  - Prepare Repulsion matrix R (take time)
2. Iteration
  - Using P matrix and R matrix to calculate G matrix
  - F matrix = H matrix + G matrix
  - Solve secular equation with F and S to get improved Co (means improved molecular orbitals)
  - Using improved Co to build improved P matrix
  - check whether converged (the change of total energy smaller than converge requirement)

```python
def run_hf(bfs, Z):
    """
    Run restricted hartree fock for a single atom.

    INPUT:
        bfs: basis functions
        Z: nuclear charge of the atom
    """
    print('------------------------------', "Initialization", '------------------------------')
    print('-------------------------', "Ignore repulsion integral", '------------------------')
    N = Z  # num of electron = nuclear charege (since it's atom)
    start = time.time()

    # initialization
    H = H_matrix(bfs, Z)
    S = S_matrix(bfs)
    e, Co = secular_eqn(H, S)
    P = P_matrix(Co, N)
    Vnn = 0  # A single atom does not have nuclear repulsion
    hf_e = energy_tot(e, N, P, H, Vnn)

    stop = time.time()
    print_info(S, H, e, Co, P, hf_e, start, stop, verbose=verbose)
    print('-----------', "Caculating Electron Repulsion Integral (takes time)", '------------')
    R = R_matrix(bfs)
    delta_e = 1
    ITER = 0
    previous_e = hf_e

    # Iterations
    while(delta_e > E_conv and ITER < MAXITER):
        print('------------------------------', "Iteration", ITER + 1, '------------------------------')
        start = time.time()

        # important scf steps
        G = G_matrix(P, R)
        F = H + G
        e, Co = secular_eqn(F, S)
        P = P_matrix(Co, N)
        hf_e = energy_tot(e, N, P, H, Vnn)

        delta_e = np.abs(hf_e - previous_e)
        previous_e = hf_e
        ITER += 1
        stop = time.time()
        print_info(S, H, e, Co, P, hf_e, start, stop, delta_e, verbose=verbose)

    return hf_e
```

Set converge criterion

```python
MAXITER = 40     # Maximum SCF iterations
E_conv = 1.0e-6  # Energy convergence criterion
verbose = False  # whether to print matrix information while iterating
```

### 10. Test

Run hartree fock for Helium

```python
def test1():
    # Use 2 Slater Type ourbital to represent Helium 1s orbital.
    # The final Helium 1s orbital is a linear combination of these two STO.
    f1s_1 = STO(zeta=1.45363, n=1)
    f1s_2 = STO(zeta=2.91093, n=1)

    # all basis functions
    fs = [f1s_1, f1s_2]

    #  nuclear charge of He
    Z = 2

    # run hartree fock
    hf_e = run_hf(fs, Z)

    # compare result with reference
    ref_hf_e = -2.8616726
    compare(hf_e, ref_hf_e)
```

```python
test1()
```

    ------------------------------ Initialization ------------------------------
    ------------------------- Ignore repulsion integral ------------------------
    HF energy: -3.95924 (hartree) = -107.73486 (eV)
    time used: 0.2 s
    ----------- Caculating Electron Repulsion Integral (takes time) ------------
    time Repu: 3.2 s
    ------------------------------ Iteration 1 ------------------------------
    HF energy: -2.78457 (hartree) = -75.77099 (eV)
    dE       : 1.17e+00
    time used: 0.0 s
    ------------------------------ Iteration 2 ------------------------------
    HF energy: -2.85860 (hartree) = -77.78532 (eV)
    dE       : 7.40e-02
    time used: 0.0 s
    ------------------------------ Iteration 3 ------------------------------
    HF energy: -2.86152 (hartree) = -77.86490 (eV)
    dE       : 2.92e-03
    time used: 0.0 s
    ------------------------------ Iteration 4 ------------------------------
    HF energy: -2.86167 (hartree) = -77.86877 (eV)
    dE       : 1.42e-04
    time used: 0.0 s
    ------------------------------ Iteration 5 ------------------------------
    HF energy: -2.86167 (hartree) = -77.86896 (eV)
    dE       : 7.01e-06
    time used: 0.0 s
    ------------------------------ Iteration 6 ------------------------------
    HF energy: -2.86167 (hartree) = -77.86897 (eV)
    dE       : 3.45e-07
    time used: 0.0 s
    -------------------------------- PASSED ---------------------------------
    cal: -2.8616726, ref: -2.8616726

Run hartree fock for Beryllium

```python
def test2():
    """
    Test of Be (1s, 2s)
    """
    # Use 2 STO to represent Be 1s orbital and another 2 STO for 2s orbital
    # The final 1s orbital is a linear combination of these 4 STO.
    # Same for 2s orbital.
    f1s_1 = STO(zeta=5.59108, n=1)
    f1s_2 = STO(zeta=3.35538, n=1)
    f2s_1 = STO(zeta=1.01122, n=2)
    f2s_2 = STO(zeta=0.61000, n=2)

    # all basis functions
    fs = [f1s_1, f1s_2, f2s_1, f2s_2]

    # nuclear charge of Be
    Z = 4

    # run hartree fock
    hf_e = run_hf(fs, Z)

    # compare result with reference
    ref_hf_e = -14.572369
    compare(hf_e, ref_hf_e)
```

```python
test2()
```

    ------------------------------ Initialization ------------------------------
    ------------------------- Ignore repulsion integral ------------------------
    HF energy: -19.51846 (hartree) = -531.11686 (eV)
    time used: 1.0 s
    ----------- Caculating Electron Repulsion Integral (takes time) ------------
    time Repu: 56.9 s
    ------------------------------ Iteration 1 ------------------------------
    HF energy: -14.28744 (hartree) = -388.77544 (eV)
    dE       : 5.23e+00
    time used: 0.0 s
    ------------------------------ Iteration 2 ------------------------------
    HF energy: -14.50425 (hartree) = -394.67518 (eV)
    dE       : 2.17e-01
    time used: 0.0 s
    ------------------------------ Iteration 3 ------------------------------
    HF energy: -14.55105 (hartree) = -395.94867 (eV)
    dE       : 4.68e-02
    time used: 0.0 s
    ------------------------------ Iteration 4 ------------------------------
    HF energy: -14.56616 (hartree) = -396.35976 (eV)
    dE       : 1.51e-02
    time used: 0.0 s
    ------------------------------ Iteration 5 ------------------------------
    HF energy: -14.57061 (hartree) = -396.48075 (eV)
    dE       : 4.45e-03
    time used: 0.0 s
    ------------------------------ Iteration 6 ------------------------------
    HF energy: -14.57187 (hartree) = -396.51521 (eV)
    dE       : 1.27e-03
    time used: 0.0 s
    ------------------------------ Iteration 7 ------------------------------
    HF energy: -14.57223 (hartree) = -396.52493 (eV)
    dE       : 3.57e-04
    time used: 0.0 s
    ------------------------------ Iteration 8 ------------------------------
    HF energy: -14.57233 (hartree) = -396.52766 (eV)
    dE       : 1.01e-04
    time used: 0.0 s
    ------------------------------ Iteration 9 ------------------------------
    HF energy: -14.57236 (hartree) = -396.52843 (eV)
    dE       : 2.83e-05
    time used: 0.0 s
    ------------------------------ Iteration 10 ------------------------------
    HF energy: -14.57237 (hartree) = -396.52865 (eV)
    dE       : 7.94e-06
    time used: 0.0 s
    ------------------------------ Iteration 11 ------------------------------
    HF energy: -14.57237 (hartree) = -396.52871 (eV)
    dE       : 2.23e-06
    time used: 0.0 s
    ------------------------------ Iteration 12 ------------------------------
    HF energy: -14.57237 (hartree) = -396.52872 (eV)
    dE       : 6.27e-07
    time used: 0.0 s
    -------------------------------- PASSED ---------------------------------
    cal: -14.5723687, ref: -14.5723690

### 11. Excise - Plot the charge density of orbitals
Write a function based on the code of `run_hf()`, plot the charge density $4 \pi r^2 |\phi_i|^2$ of all the orbitals (final iteration).

For example, Beryllium:
Note: You only need to plot the final one (most bottom)
Hint see below.

Hint: convert a sympy expression to python function by using [sp.lambdify()](https://docs.sympy.org/latest/modules/numeric-computation.html#lambdify)
For example, to plot $\chi_1$, which is the 1st wavefunction of Beryllium Basis set.

```python
f1s_1 = STO(zeta=5.59108, n=1)
display(f1s_1)
f = sp.lambdify(r, f1s_1, "numpy")
```

```python
x = np.linspace(0, 5, 300)
y = f(x)
plt.figure(figsize=(16,2))
plt.plot(x, y, label='$\chi_{}$'.format(1))
plt.legend()
plt.xlim(0, x[-1])
plt.xlabel('r')
plt.ylabel('$\chi_1$')
plt.title('Wavefunction of $\chi_1$')
plt.show()
```

Code below is example solution

```python
def run_hf(bfs, Z):
    """
    Run restricted hartree fock for a single atom.

    INPUT:
        bfs: basis functions
        Z: nuclear charge of the atom
    """
    print('------------------------------', "Initialization", '------------------------------')
    print('-------------------------', "Ignore repulsion integral", '------------------------')
    N = Z  # num of electron = nuclear charege (since it's atom)
    start = time.time()

    # initialization
    H = H_matrix(bfs, Z)
    S = S_matrix(bfs)
    e, Co = secular_eqn(H, S)
    P = P_matrix(Co, N)
    Vnn = 0  # A single atom does not have nuclear repulsion
    hf_e = energy_tot(e, N, P, H, Vnn)

    stop = time.time()
    print_info(S, H, e, Co, P, hf_e, start, stop, verbose=verbose)
    print('-----------', "Caculating Electron Repulsion Integral (takes time)", '------------')
    R = R_matrix(bfs)
    delta_e = 1
    ITER = 0
    previous_e = hf_e

    densities = [] # [[d1, d2], [d1, d2], [d1, d2]]
    # plot
    x = np.linspace(0, 5, 300)
    tmp_density = get_density(bfs, Co, x)
    densities.append(tmp_density)

    # Iterations
    while(delta_e > E_conv and ITER < MAXITER):
        print('------------------------------', "Iteration", ITER + 1, '------------------------------')
        start = time.time()

        # important scf steps
        G = G_matrix(P, R)
        F = H + G
        e, Co = secular_eqn(F, S)
        P = P_matrix(Co, N)
        hf_e = energy_tot(e, N, P, H, Vnn)

        delta_e = np.abs(hf_e - previous_e)
        previous_e = hf_e
        ITER += 1
        stop = time.time()
        print_info(S, H, e, Co, P, hf_e, start, stop, delta_e, verbose=verbose)

        # plot
        tmp_density = get_density(bfs, Co, x)
        densities.append(tmp_density)

    plot_density(densities, x)

    return hf_e
```

```python
def get_density(bfs, Co, x):
    r = sp.Symbol('r')
    density = []

    # all orbitals
    for i in range(Co.shape[0]):
        tmp_orbital = 0
        # all basis functions
        for j, f in enumerate(bfs):
            tmp_orbital += Co[j][i] * bfs[j]
        tmp_d_function = tmp_orbital * tmp_orbital * r * r * 4 * np.pi
        tmp_f = sp.lambdify(r, tmp_d_function, "numpy")
        tmp_d = tmp_f(x)
        density.append(tmp_d)
    return density
```

```python
def plot_density(densities, x):
    num_orbitals = len(densities[0])
    num_iterations = len(densities)

    for i in range(num_orbitals):
        plt.figure(figsize=(16,2))
        for j, d in enumerate(densities):
            if j == 0 or j == (num_iterations - 1):
                plt.plot(x, d[i], label='iteration {}'.format(j+1))
            else:
                plt.plot(x, d[i])
        plt.legend()
        plt.xlim(0, x[-1])
        plt.xlabel('r')
        plt.ylabel('$4 \pi r^2 |\phi_{}|^2$'.format(i+1))
        plt.title('charge density of $\phi_{}$'.format(i+1))
        plt.show()

    plt.figure(figsize=(16,2))
    for i, d in enumerate(densities[-1]):
        plt.plot(x, d, label='$\phi_{}$'.format(i+1))
    plt.legend()
    plt.xlim(0, x[-1])
    plt.xlabel('r')
    plt.ylabel('$4 \pi r^2 |\phi_i|^2$')
    plt.title('charge density of all orbitals $\phi_i$ (final iteration)')
    plt.show()
```

```python
test1()
```

```python
test2()
```

### 12. Limitations
Limitations of this implementation:
1. Because of the coordinate system (Spherical coordinate system), this implementation can only deal with atom.
1. Because angular part is not included in current STO function, only atoms which only have s orbital could be represented by STO.
1. For restricted hartree fock, could only run closed shell atom.
1. Integral calculated by sympy, which is slow but easy to understand.

### 13. Reference
[1] Levine, Quantum Chemistry, 7th Edition, chapter 14
[2] Wikipedia
[Hartree–Fock method](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method#Hartree%E2%80%93Fock_algorithm),
[Fock matrix](https://en.wikipedia.org/wiki/Fock_matrix),
[Roothaan equations](https://en.wikipedia.org/wiki/Roothaan_equations),
[Coulomb operator](https://en.wikipedia.org/wiki/Coulomb_operator),
[Exchange operator](https://en.wikipedia.org/wiki/Exchange_operator)
[3] Clementi, Enrico, and Carla Roetti. [Roothaan-Hartree-Fock atomic wavefunctions: Basis functions and their coefficients for ground and certain excited states of neutral and ionized atoms, Z≤ 54.](https://www.sciencedirect.com/science/article/pii/S0092640X74800161) Atomic data and nuclear data tables 14.3-4 (1974): 177-478.
[4] Acosta C R. [Restricted closed shell Hartree Fock Roothaan matrix method applied to Helium atom using Mathematica[J].](https://files.eric.ed.gov/fulltext/EJ1051495.pdf) European Journal of Physics Education, 2017, 5(1): 1-14.
[5] Jacob Martin, [Simple Quantum Chemistry: Hartree-Fock in Python](http://nznano.blogspot.com/2018/03/simple-quantum-chemistry-hartree-fock.html), 2018
[6] Claire Vallance, [Calculating Orbital Energies and Expansion Coefficients](https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Book%3A_Symmetry_(Vallance)/20%3A_Calculating_Orbital_Energies_and_Expansion_Coefficients), 2019


##### Copyright 2020 The TensorFlow Authors.

```
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
```

# こんにちは、多くの世界

  View on TensorFlow.org
  Run in Google Colab
  GitHub でソースを表示{
  ノートブックをダウンロード/a0}

このチュートリアルでは、古典的なニューラルネットワークが量子ビット・キャリブレーションエラーの訂正を学習する方法を紹介します。Circq は NISQ（ノイズの多い中間スケール量子）回路を作成、編集、呼び出すための Python フレームワークであり、ここでは Cirq が TensorFlow Quantum とどのようにやり取りするかを示します。

## セットアップ

```
!pip install tensorflow==2.1.0
```

TensorFlow Quantum をインストールします。

```
!pip install tensorflow-quantum
```

次に、TensorFlow とモジュールの依存関係をインポートします。

```
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

## 1. 基本

### 1.1 Cirq とパラメータ化された量子回路

TensorFlow Quantum (TFQ) について説明する前に、Circq の基本をいくつか見てみましょう。Cirq は、Google の量子コンピューティング用の Python ライブラリで、静的ゲートやパラメータ化されたゲートなどの回路の定義に使用します。

Cirq は、SymPy シンボルを使用して自由パラメータを表します。

```
a, b = sympy.symbols('a b')
```

次のコードは、上記のパラメータを使用して 2 つの量子ビット回路を作成します。

```
# Create two qubits
q0, q1 = cirq.GridQubit.rect(1, 2)

# Create a circuit on these qubits using the parameters you created above.
circuit = cirq.Circuit(
    cirq.rx(a).on(q0),
    cirq.ry(b).on(q1), cirq.CNOT(control=q0, target=q1))

SVGCircuit(circuit)
```

回路を評価するには、`cirq.Simulator`インターフェースを使用します。回路内の自由パラメータを特定の数値に置き換えるには、`cirq.ParamResolver`オブジェクトを渡します。以下のコードは、パラメータ化された回路の生の状態ベクトル出力を計算します。

```
# Calculate a state vector with a=0.5 and b=-0.5.
resolver = cirq.ParamResolver({a: 0.5, b: -0.5})
output_state_vector = cirq.Simulator().simulate(circuit, resolver).final_state
output_state_vector
```

状態ベクトルは、シミュレーションの外から直接アクセスすることはできません（上記の複素数出力に注意してください）。物理的に現実的にするには、状態ベクトルを古典的コンピュータが理解できる実数に変換する測定値を指定する必要があります。Cirq は、Pauli 演算子 $\hat{X}$, $\hat{Y}$ および $\hat{Z}$ の組み合わせを使用して測定値を指定します。例として、次のコードは、シミュレーションした状態ベクトルで $\hat{Z}_0$ と $\frac{1}{2}\hat{Z}_0 + \hat{X}_1$ を測定します。

```
z0 = cirq.Z(q0)

qubit_map={q0: 0, q1: 1}

z0.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

```
z0x1 = 0.5 * z0 + cirq.X(q1)

z0x1.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

### 1.2 テンソルとしての量子回路

TensorFlow Quantum (TFQ) は、Cirq オブジェクトをテンソルに変換する関数である`tfq.convert_to_tensor`を提供します。これにより、Cirq オブジェクトを量子レイヤーおよび量子演算に送信できます。この関数は、Cirq Circuits と Cirq Paulis のリストまたは配列で呼び出すことができます。

```
# Rank 1 tensor containing 1 circuit.
circuit_tensor = tfq.convert_to_tensor([circuit])

print(circuit_tensor.shape)
print(circuit_tensor.dtype)
```

これは、Cirq オブジェクトを`tf.string`テンソルとしてエンコードし、`tfq`演算は必要に応じてデコードします。

```
# Rank 1 tensor containing 2 Pauli operators.
pauli_tensor = tfq.convert_to_tensor([z0, z0x1])
pauli_tensor.shape
```

### 1.3 バッチ回路シミュレーション

TFQ は、期待値、サンプル、および状態ベクトルを計算するためのメソッドを提供します。まず、*期待値*から見ていきましょう。

期待値を計算するための最高レベルのインターフェースは、`tf.keras.Layer`である`tfq.layers.Expectation`レイヤーです。最も単純な形式では、このレイヤーは、多くの`cirq.ParamResolvers`でパラメータ化された回路をシミュレートすることと同等ですが、TFQ では TensorFlow セマンティクスに従ったバッチ処理が可能であり、回路は効率的な C++ コードを使用してシミュレートされます。

`a`と`b`パラメータの代わりに値のバッチを作成します。

```
batch_vals = np.array(np.random.uniform(0, 2 * np.pi, (5, 2)), dtype=np.float32)
```

Cirq のパラメータ値に対するバッチ回路の実行には、ループが必要です。

```
cirq_results = []
cirq_simulator = cirq.Simulator()

for vals in batch_vals:
    resolver = cirq.ParamResolver({a: vals[0], b: vals[1]})
    final_state = cirq_simulator.simulate(circuit, resolver).final_state
    cirq_results.append(
        [z0.expectation_from_wavefunction(final_state, {
            q0: 0,
            q1: 1
        }).real])

print('cirq batch results: \n {}'.format(np.array(cirq_results)))
```

TFQ では同じ演算が簡略化されています。

```
tfq.layers.Expectation()(circuit,
                         symbol_names=[a, b],
                         symbol_values=batch_vals,
                         operators=z0)
```

## 2. 量子古典ハイブリッドの最適化

以上は基本の説明でした。次に、TensorFlow Quantum を使用して*量子古典ハイブリッドニューラルネット*を構築しましょう。古典的なニューラルネットをトレーニングして、1 つの量子ビットを制御します。コントロールは、`0`または`1`の状態の量子ビットを正しく準備するように最適化され、シミュレートされた系統的なキャリブレーションエラーを克服します。以下の図は、アーキテクチャを示しています。

これはニューラルネットワークがなくても簡単に解決できる問題ですが、テーマは TFQ を使用して解決できる実際の量子制御の問題と似ています。これは、`tf.keras.Model`内の`tfq.layers.ControlledPQC` (Parametrized Quantum Circuit) レイヤーを使用した量子古典計算のエンドツーエンドの例を示しています。

このチュートリアルの実装では、アーキテクチャは 3 つの部分に分かれています。

- *入力回路*または*データポイント回路*：最初の 3 つの $R$ ゲート。
- *制御回路*：その他の 3 つの $R$ ゲート。
- *コントローラ*：制御回路のパラメータを設定する古典的なニューラルネットワーク。

### 2.1 制御回路の定義

上の図に示すように、学習可能なシングルビットローテーションを定義します。これは、制御回路に対応します。

```
# Parameters that the classical NN will feed values into.
control_params = sympy.symbols('theta_1 theta_2 theta_3')

# Create the parameterized circuit.
qubit = cirq.GridQubit(0, 0)
model_circuit = cirq.Circuit(
    cirq.rz(control_params[0])(qubit),
    cirq.ry(control_params[1])(qubit),
    cirq.rx(control_params[2])(qubit))

SVGCircuit(model_circuit)
```

### 2.2 コントローラ

次に、コントローラネットワークを定義します。

```
# The classical neural network layers.
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])
```

コントローラにコマンドのバッチを与えると、制御された回路の制御信号のバッチが出力されます。

コントローラはランダムに初期化されるため、これらの出力はまだ有用ではありません。

```
controller(tf.constant([[0.0],[1.0]])).numpy()
```

### 2.3 コントローラを回路に接続する

`tfq`を使用して、コントローラを 1 つの`keras.Model`として制御回路に接続します。

このスタイルのモデル定義の詳細については、[Keras Functional API ガイド](https://www.tensorflow.org/guide/keras/functional)をご覧ください。

まず、モデルへの入力を定義します。

```
# This input is the simulated miscalibration that the model will learn to correct.
circuits_input = tf.keras.Input(shape=(),
                                # The circuit-tensor has dtype `tf.string`
                                dtype=tf.string,
                                name='circuits_input')

# Commands will be either `0` or `1`, specifying the state to set the qubit to.
commands_input = tf.keras.Input(shape=(1,),
                                dtype=tf.dtypes.float32,
                                name='commands_input')

```

次に、これらの入力に演算を適用して、計算を定義します。

```
dense_2 = controller(commands_input)

# TFQ layer for classically controlled circuits.
expectation_layer = tfq.layers.ControlledPQC(model_circuit,
                                             # Observe Z
                                             operators = cirq.Z(qubit))
expectation = expectation_layer([circuits_input, dense_2])
```

次に、この計算を`tf.keras.Model`としてパッケージ化します。

```
# The full Keras model is built from our layers.
model = tf.keras.Model(inputs=[circuits_input, commands_input],
                       outputs=expectation)
```

ネットワークアーキテクチャは、以下のモデルのプロットで示されています。このモデルプロットをアーキテクチャ図と比較して、正確さを確認します。

注意: `graphviz`パッケージのシステムインストールが必要になる場合があります。

```
tf.keras.utils.plot_model(model, show_shapes=True, dpi=70)
```

このモデルは、コントローラのコマンドと、コントローラが出力を修正しようとしている入力回路の 2 つの入力を受け取ります。

### 2.4 データセット

モデルは、コマンドごとに $\hat{Z}$ の正しい測定値の出力を試行します。コマンドと正しい値の定義は以下のとおりです。

```
# The command input values to the classical NN.
commands = np.array([[0], [1]], dtype=np.float32)

# The desired Z expectation value at output of quantum circuit.
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
```

これは、このタスクのトレーニングデータセット全体ではありません。データセット内の各データポイントにも入力回路が必要です。

### 2.4 入力回路の定義

以下の入力回路は、モデルが修正することを学習するためのランダムな誤校正を定義します。

```
random_rotations = np.random.uniform(0, 2 * np.pi, 3)
noisy_preparation = cirq.Circuit(
  cirq.rx(random_rotations[0])(qubit),
  cirq.ry(random_rotations[1])(qubit),
  cirq.rz(random_rotations[2])(qubit)
)
datapoint_circuits = tfq.convert_to_tensor([
  noisy_preparation
] * 2)  # Make two copied of this circuit
```

回路には 2 つのコピーがあります（データポイントごとに 1 つずつ）。

```
datapoint_circuits.shape
```

### 2.5 トレーニング

定義された入力を使用して、`tfq`モデルのテストランを実行します。

```
model([datapoint_circuits, commands]).numpy()
```

次に、標準のトレーニングプロセスを実行して、これらの値を`expected_outputs`に向けて調整します。

```
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()
model.compile(optimizer=optimizer, loss=loss)
history = model.fit(x=[datapoint_circuits, commands],
                    y=expected_outputs,
                    epochs=30,
                    verbose=0)
```

```
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

このプロットから、ニューラルネットワークが体系的なキャリブレーションエラーを訂正することを学習したことがわかります。

### 2.6 出力の確認

次に、トレーニング済みモデルを使用して、量子ビット・キャリブレーションエラーを修正します。Cirq を使用する場合は以下のとおりです。

```
def check_error(command_values, desired_values):
  """Based on the value in `command_value` see how well you could prepare
  the full circuit to have `desired_value` when taking expectation w.r.t. Z."""
  params_to_prepare_output = controller(command_values).numpy()
  full_circuit = noisy_preparation + model_circuit

  # Test how well you can prepare a state to get expectation the expectation
  # value in `desired_values`
  for index in [0, 1]:
    state = cirq_simulator.simulate(
        full_circuit,
        {s:v for (s,v) in zip(control_params, params_to_prepare_output[index])}
    ).final_state
    expectation = z0.expectation_from_wavefunction(state, {qubit: 0}).real
    print(f'For a desired output (expectation) of {desired_values[index]} with'
          f' noisy preparation, the controller\nnetwork found the following '
          f'values for theta: {params_to_prepare_output[index]}\nWhich gives an'
          f' actual expectation of: {expectation}\n')

check_error(commands, expected_outputs)
```

トレーニング中の損失関数の値から、モデルの学習がどれほど進んでいるかが大まかに分かります。損失が小さいほど、上記のセルの期待値は`Desired_values`に近くなります。パラメータ値に関心がない場合は、`tfq`を使用して上記からの出力をいつでも確認できます。

```
model([datapoint_circuits, commands])
```

## 3 さまざまな演算子の固有状態の準備について学ぶ

1 と 0 に対応する $\pm \hat{Z}$ 固有状態の選択は任意でした。1 を $+ \hat{Z}$ 固有状態に対応させ、0 を $-\hat{X}$ 固有状態に対応させることも簡単にできます。そのためには、次の図に示すように、コマンドごとに異なる測定演算子を指定します。

これには、tfq.layers.Expectationを使用する必要があります。これで、入力は、回路、コマンド、および演算子の 3 つのオブジェクトを含むようになりました。出力は期待値のままです。

### 3.1 新しいモデルの定義

このタスクを実行するためのモデルを見てみましょう。

```
# Define inputs.
commands_input = tf.keras.layers.Input(shape=(1),
                                       dtype=tf.dtypes.float32,
                                       name='commands_input')
circuits_input = tf.keras.Input(shape=(),
                                # The circuit-tensor has dtype `tf.string`
                                dtype=tf.dtypes.string,
                                name='circuits_input')
operators_input = tf.keras.Input(shape=(1,),
                                 dtype=tf.dtypes.string,
                                 name='operators_input')
```

コントローラネットワークは次のとおりです。

```
# Define classical NN.
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])
```

`tfq`を使用して、回路とコントローラを 1 つの`keras.Model`に結合します。

```
dense_2 = controller(commands_input)

# Since you aren't using a PQC or ControlledPQC you must append
# your model circuit onto the datapoint circuit tensor manually.
full_circuit = tfq.layers.AddCircuit()(circuits_input, append=model_circuit)
expectation_output = tfq.layers.Expectation()(full_circuit,
                                              symbol_names=control_params,
                                              symbol_values=dense_2,
                                              operators=operators_input)

# Contruct your Keras model.
two_axis_control_model = tf.keras.Model(
    inputs=[circuits_input, commands_input, operators_input],
    outputs=[expectation_output])
```

### 3.2 データセット

`model_circuit`に提供する各データポイントに対して測定する演算子も含めます。

```
# The operators to measure, for each command.
operator_data = tfq.convert_to_tensor([[cirq.X(qubit)], [cirq.Z(qubit)]])

# The command input values to the classical NN.
commands = np.array([[0], [1]], dtype=np.float32)

# The desired expectation value at output of quantum circuit.
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
```

### 3.3トレーニング

新しい入力と出力を使用し、keras でもう一度トレーニングします。

```
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()

two_axis_control_model.compile(optimizer=optimizer, loss=loss)

history = two_axis_control_model.fit(
    x=[datapoint_circuits, commands, operator_data],
    y=expected_outputs,
    epochs=30,
    verbose=1)
```

```
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

損失関数はゼロに低下しました。

`controller`はスタンドアロンモデルとして利用できます。コントローラを呼び出し、各コマンド信号に対する応答を確認します。多少手間がかかりますが、これらの出力を`random_rotations`の内容と比較します。

```
controller.predict(np.array([0,1]))
```

成功: 最初のモデルの`check_error`関数を、この新しいモデルアーキテクチャで動作するように適合させることができるかどうかを確認します。