# *Ab* *initio* molecular dynamics of the vibrational motion of HF

### Part 1:  Generation of *ab* *initio* potential energy surfaces (PES)
We are going to construct what is often referred to as an *ab* *initio* potential energy surface of the diatomic
molecule hydrogen fluoride.  That is, we are going to use various electronic structure theories (Hartree-Fock theory (RHF), 2nd-order perturbation theory (MP2), and Coupled Cluster theory with single and double substitutions (CCSD)) to compute the electronic energy at different geometries of a simple diatomic molecule.  The same basis set (correlation consistent polarized triple-zeta, cc-pVTZ) will be used for all calculations.  We will use Psi4numpy to facilitate the electronic structure calculations, and then the interpolation capabilities of scipy to simplify the evalution of the potential energy at separations for which we did not explicitly evaluate the electronic energy.  We will also use scipy to differentiate the interpolated potential energy surface to obtain the forces acting on the atoms at different separations.

We will start by importing the necessary libraries:

```python
"""This will be pre-written"""
import numpy as np
import psi4
from matplotlib import pyplot as plt
from scipy.interpolate import InterpolatedUnivariateSpline
```

We will use a template for the z-matrix which will allow us to automate the
specification of the bond length of our HF molecule for easy computation of our potential
energy surface.

```python
"""This will be pre-written"""
### template for the z-matrix
mol_tmpl = """H
F 1 **R**"""
```

Now let's create arrays for the bond length and energies at each bond length
for three different levels of theory (RHF, MP2, and CCSD).  Let's have our bond lengths
spane 0.5 - 2.25 $\overset{\circ}{A}$; note that should use finer resolution for short bondlengths than our longer bondlengths because we want to be sure we accurately represent the minimum energy point on the PES!

```python
''' Have students write this block! '''
### array of bond-lengths in anstromgs
r_array = np.array([0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3])
### array for different instances of the HF molecule
molecules =[]
### array for the different RHF energies for different HF bond-lengths
RHF_E_array = []
### array for the different MP2 energies for different HF bond-lengths
MP2_E_array = []
### array for the different CCSD energies for different HF bond-lengths
CCSD_E_array = []

```

Now we will loop over the elements of our r_array, compute the energies at each level of theory for all bond lengths, and store them in their respective arrays.

```python
"""This will be pre-written"""
### loop over the different bond-lengths, create different instances
### of HF molecule
for r in r_array:
    molecule = psi4.geometry(mol_tmpl.replace("**R**", str(r)))
    molecules.append(molecule)

### loop over instances of molecules, compute the RHF, MP2, and CCSD
### energies and store them in their respective arrays
for mol in molecules:
    energy = psi4.energy("SCF/cc-pVTZ", molecule=mol)
    RHF_E_array.append(energy)
    energy = psi4.energy("MP2/cc-pVTZ", molecule=mol)
    MP2_E_array.append(energy)
    energy = psi4.energy("CCSD/cc-pVTZ",molecule=mol)
    CCSD_E_array.append(energy)

#print(r_array, RHF_E_array, MP2_E_array, CCSD_E_array)

```

```python
for i in range(0,len(r_array)):
    print(r_array[i], RHF_E_array[i], MP2_E_array[i], CCSD_E_array[i])
#print(r_array, RHF_E_array, MP2_E_array, CCSD_E_array)
''' Have students write this block! '''
### Plot the 3 different PES
plt.plot(r_array,RHF_E_array,'r*', label='RHF')
plt.plot(r_array,MP2_E_array,'g*', label='MP2')
plt.plot(r_array,CCSD_E_array,'b*', label='CCSD')
plt.legend()
```

Now that you have the raw data, we will interpolate this data using cubic splines.  This will permit us to
estimate the potential energy at any arbitrary separation between 0.5 and 2.25 Angstroms.

The general syntax for creating a cubic spline object is as follows:

`spline = InterpolatedUnivariateSpline(x-data, y-data, k=3)`

#### Note on units
The energies we obtained from psi4 are in Hartrees, which are the atomic unit of energy.  We have so far been specifying our separation in Angstroms (**not the atomic unit of length**) so we are in a mixed unit system.  When we generate our spline, we will use an array of bond lengths in atomic units as the x-data and the energies in atomic units as the y-data, which will yield a PES purely in atomic units.  Therefore, the first thing we will do before creating the spline is to create an array of bond lengths in atomic units (~1.89 * bond lengths in Angstroms is the bond length in atomic units); we will then create three cubic splines (RHF_E_Spline, MP2_E_Spline, CCSD_E_SPline) that hold the PES data in atomic units for the three levels of theory.

```python
''' Have students write this block! '''
### get separation vector in atomic units
r_array_au = 1.89*r_array

### spline for RHF Energy
RHF_E_Spline = InterpolatedUnivariateSpline(r_array_au, RHF_E_array, k=3)

### spline for MP2 Energy
MP2_E_Spline = InterpolatedUnivariateSpline(r_array_au, MP2_E_array, k=3)

### spline for CCSD Energy
CCSD_E_Spline = InterpolatedUnivariateSpline(r_array_au, CCSD_E_array, k=3)
```

Now we can plot the splines against the PES data to make sure our splines were generated properly.

```python
''' This will be pre-written '''
### form a much finer grid to evaluate spline object at
r_fine = np.linspace(0.5/0.529,2.3/0.529,200)

### compute the interpolated/extrapolated values for RHF Energy on this grid
RHF_E_fine = RHF_E_Spline(r_fine)

### compute the interpolated/extrapolated values for RHF Energy on this grid
MP2_E_fine = MP2_E_Spline(r_fine)

### compute the interpolated/extrapolated values for RHF Energy on this grid
CCSD_E_fine = CCSD_E_Spline(r_fine)

### plot the interpolated data with lines against computed data in *'s
plt.plot(r_fine, RHF_E_fine, 'red', r_array_au, RHF_E_array, 'r*', label='RHF')
plt.plot(r_fine, MP2_E_fine, 'green', r_array_au, MP2_E_array, 'g*', label='MP2')
plt.plot(r_fine, CCSD_E_fine, 'blue', r_array_au, CCSD_E_array, 'b*', label='CCSD')
plt.legend()
plt.show()
```

### Part 2:  Computation of Forces and related quantities and their importance in Newton's law
We can derive a number of important quantities just from the potential energy surfaces we have computed.  For example, we estimate the equilibrium bond length by finding the separation at which the potential is minimum; note this would also be the position that the force goes to zero:
\begin{equation}
\frac{d}{dr} V(r_{eq}) = -F(r_{eq}) = 0.
\end{equation}

The force as a function of separation plays a significant role in the vibrational motion of the molecule, as we will see shortly.

First we will compute the forces at each level of theory, storing them in new spline
onjects called RHF_Force, MP2_Force, and CCSD_Force. We can use the fact
that the spline objects (which we previously created) can be directly differentiated using the following syntax:

`spline_derivative = spline.derivative()`

Once computed, plot each spline against the r_fine array previously created!
#### What unit system do you think the forces are in?

```python
''' Have students write this block! '''
### take the derivative of the potential to get the negative of the force from RHF
RHF_Force = RHF_E_Spline.derivative()

### negative of the force from MP2
MP2_Force = MP2_E_Spline.derivative()

### negative of the force from CCSD
CCSD_Force = CCSD_E_Spline.derivative()

### let's plot the forces for each level of theory!

### plot the forces... note we need to multiply by -1 since the spline
### derivative gave us the negative of the force!
plt.plot(r_fine, -1*RHF_Force(r_fine), 'red', label='RHF Force')
plt.plot(r_fine, -1*MP2_Force(r_fine), 'green', label='MP2 Force')
plt.plot(r_fine, -1*CCSD_Force(r_fine), 'blue', label='CCSD Force')
plt.legend()
plt.show()
```

#### Equilibrium bond length
Next we will find where the minimum of the potential energy surfaces are and use that
to find the equilibrium bond length, making use of numpy's argmin function to find the
index corresponding to the minimum value in a numpy array:

```python
''' This block will be pre-written! '''
### Find Equilibrium Bond-Lengths for each level of theory
RHF_Req_idx = np.argmin(RHF_E_fine)
MP2_Req_idx = np.argmin(MP2_E_fine)
CCSD_Req_idx = np.argmin(CCSD_E_fine)

### find the value of the separation corresponding to that index
RHF_Req = r_fine[RHF_Req_idx]
MP2_Req = r_fine[MP2_Req_idx]
CCSD_Req = r_fine[CCSD_Req_idx]

### print equilibrium bond-lengths at each level of theory!
print(" Equilibrium bond length at RHF/cc-pVDZ level is ",RHF_Req, "atomic units")
print(" Equilibrium bond length at MP2/cc-pVDZ level is ",MP2_Req, "atomic units")
print(" Equilibrium bond lengthat CCSD/cc-pVDZ level is ",CCSD_Req, "atomic units")

```

     Equilibrium bond length at RHF/cc-pVDZ level is  1.6975235344966797 atomic units
     Equilibrium bond length at MP2/cc-pVDZ level is  1.7317209867864842 atomic units
     Equilibrium bond lengthat CCSD/cc-pVDZ level is  1.7317209867864842 atomic units

#### At this point, take a moment to compare your equilibrium bond length by level of theory.  Which equilibrium bond length do you expect to be most trustworthy?  Is it the case that this method produced the **best** equilibrium bond length in this case?  Note that the experimental bond length of HF is ~0.92 $\overset{\circ}{A}$.

#### Harmonic Frequency
You might have learned that the Harmonic Oscillator potential, which is a reasonable model for the vibrational motion of diatomic atomcs near their equilibrium bond length, is given by
\begin{equation}
V(r) = \frac{1}{2} k (r-r_{eq})^2 + V_0
\end{equation}
and that the vibrational frequency of the molecule within the Harmonic oscillator model is given by
\begin{equation}
\nu = \frac{1}{2\pi}\sqrt{\frac{k}{\mu}}
\end{equation}
where $\mu$ is the reduced mass of the molecule and $k$ is known as the force constant.
We can estimate the force constant as
\begin{equation}
k = \frac{d^2}{dr^2} V(r_{eq}),
\end{equation}
and the reduced mass of HF is defined as
\begin{equation}
\mu = \frac{m_H \cdot m_F}{m_H + m_F},
\end{equation}
where $m_H$ and $m_F$ are the masses of Hydrogen and Fluoride, respectively.

Let's go ahead and get the force constants at each level of theory, print the values,
and estimate the potential energy within the Harmonic approximation!  Just like we were able to differentiate our PES splines to get a force spline, we can differentiate a force splines to get curvature splines (which we can call RHF_Curvature, MP2_Curvature, and CCSD_Curvature); the force constant will then be the curvature evaluated at the equlibrium bond length.

#### Can we use the same equilibrium bond length for all three curvatures?  Why or why not?

```python
''' Have students write this block! '''
### get second derivative of potential energy curve... recall that we fit a spline to
### to the first derivative already and called that spline function X_Force, where
### X is either RHF, MP2, or CCSD

RHF_Curvature = RHF_Force.derivative()
MP2_Curvature = MP2_Force.derivative()
CCSD_Curvature = CCSD_Force.derivative()

### evaluate the second derivative at r_eq to get k
RHF_k = RHF_Curvature(RHF_Req)
MP2_k = MP2_Curvature(MP2_Req)
CCSD_k = CCSD_Curvature(CCSD_Req)

### Print force constants for each level of theory!
print("Hartree-Fock force constant is ",RHF_k," atomic units")
print("MP2 force constant is ",MP2_k," atomic units")
print("CCSD force constant is ",CCSD_k," atomic units")

```

    Hartree-Fock force constant is  0.7201715614223737  atomic units
    MP2 force constant is  0.6423033492324176  atomic units
    CCSD force constant is  0.6393333544724277  atomic units

Now that we have the force constants, let's define three different arrays (RHF_Harm_Pot, MP2_Harm_Pot, and CCSD_Harm_Pot) that store the harmonic potentials at each level of theory evaluated at the different bond lengths (in atomic units) stored in the array r_fine; recall the definition of the Harmonic potential is
\begin{equation}
V(r) = \frac{1}{2} k (r-r_{eq})^2 + V_0,
\end{equation}
where we can use $E(r_{eq})$ as $V_0$.

```python
''' Have students write this block! '''
### define harmonic potential for each level of theory
RHF_Harm_Pot = 0.5*RHF_k*(r_fine-RHF_Req)**2 + RHF_E_Spline(RHF_Req)
MP2_Harm_Pot = 0.5*MP2_k*(r_fine-MP2_Req)**2 + MP2_E_Spline(MP2_Req)
CCSD_Harm_Pot = 0.5*CCSD_k*(r_fine-CCSD_Req)**2 + CCSD_E_Spline(CCSD_Req)

```

Let's plot the resulting Harmonic potentials against the *ab* *initio* potentials near the equilibrium geometry

```python
''' This will be pre-written!'''
### plot!
plt.plot(r_fine, RHF_Harm_Pot, 'red', label='Harmonic')
plt.plot(r_fine, RHF_E_fine, 'b--', label='ab initio' )
plt.xlim(1.0, (1.69+0.69))
plt.ylim(-100.1,-99.6)
plt.legend()
plt.show()
```

Finally, let's actually estimate the fundamental vibrational frequency of the molecule
within this model using the force constant and the reduced mass of the molecule.

####  What is the reduced mass of the HF molecule in atomic units?

#### Use your Harmonic force constants to estimate the vibrational frequency of HF and compare the values obtained from each level of theory.  Which frequency do you think should be the most trusthworthy?  Is it the case that this method produced the best frequency in this case?  Note that the experimental vibrational frequency of HF is 124 THz.

```python
''' Have students write this block! '''
### define reduced mass of HF as m_H * m_H /(m_F + m_H) where mass is in atomic units (electron mass = 1)
m_F = 34883.
m_H = 1836.
mu = (m_F * m_H)/(m_F + m_H)

### compute the fundamental frequency at each level of theory
RHF_nu = 1/(np.pi*2) * np.sqrt(RHF_k/mu)
MP2_nu = 1/(np.pi*2) * np.sqrt(MP2_k/mu)
CCSD_nu = 1/(np.pi*2) * np.sqrt(CCSD_k/mu)

### print the values in atomic units!
print("Vibrational frequency of HF at the RHF/cc-pVDZ level is ",RHF_nu," atomic units")
print("Vibrational frequency of HF at the MP2/cc-pVDZ level is ",MP2_nu," atomic units")
print("Vibrational frequency of HF at the CCSD/cc-pVDZ level is ",CCSD_nu," atomic units")

```

    Vibrational frequency of HF at the RHF/cc-pVDZ level is  0.0032340020112283717  atomic units
    Vibrational frequency of HF at the MP2/cc-pVDZ level is  0.0030541642910292405  atomic units
    Vibrational frequency of HF at the CCSD/cc-pVDZ level is  0.0030470949194360422  atomic units

### Part 3: Solving Newton's equation of motion to simulate the dynamics
Next, we want to actually simulate the dynamics of the HF molecule on these *ab* *initio* potential energy surfaces.  To do so, we need to solve Newton's equations of motion subject to some initial condition for the position (separation) and momentum (in a relative sense) of the particles.  Newton's equations can be written
\begin{equation}
F(r) = \mu \frac{d^2}{dr^2}
\end{equation}
where $\mu$ is the reduced mass in atomic units and $F(r)$ is the Force vs separation in atomic units that was determined previously.

#### What will be the accelation of the bond stretch when H is separated by F by 3 atomic units?  You can express your acceleration in atomic units, also.

```python
""" Students will write this line! """
accel = RHF_Force(3)/mu
print(accel)
```

    7.807549838748958e-05

#### Numerically solving Newton's equation of motion
If the acceleration, position, and velocity of the bond stretch coordinate are known at some instant in
time $t_i$, then the position and velocity can be estimated at some later time $t_{i+1} = t_i + \Delta t$:
\begin{equation}
r(t_i + \Delta t) = r(t_i) + v(t_i)\Delta t + \frac{1}{2}a(t_i)\Delta t^2
\end{equation}
and
\begin{equation}
v(t_i + \Delta t) = v(t_i) + \frac{1}{2} \left(a(t_i) + a(t_i + \Delta t)  \right) \Delta t.
\end{equation}
This prescription for updating the velocities and positions is known as the Velocity-Verlet algorithm.
Note that we need to perform 2 force evaluations per Velocity-Verlet iteration: one corresponding
to position $r(t_i)$ to update the position, and then a second time at the updated position $r(t_i + \Delta t)$
to complete the velocity update.

We will create a function called Velocity_Verlet that takes the arguments r_curr, v_curr, mu, force_spline, and timestep and returns a 2-element array containing the updated position (r) and velocity (v) value.

```python
''' Students will write this! '''
def Velocity_Verlet(r_curr, v_curr, mu, f_interp, dt):
    ### get acceleration at current time
    a_curr = -1*f_interp(r_curr)/mu

    ### use current acceleration and velocity to update position
    r_fut = r_curr + v_curr * dt + 0.5 * a_curr * dt**2

    ### use r_fut to get future acceleration a_fut
    a_fut = -1*f_interp(r_fut)/mu
    ### use current and future acceleration to get future velocity v_fut
    v_fut = v_curr + 0.5*(a_curr + a_fut) * dt

    result = [r_fut, v_fut]

    return result
```

### Validating Velocity-Verlet algorithm with the Harmonic Oscillator
Newton's equation of motion can be solved analytically for the Harmonic oscillator, and we can use this fact to validate our Velocity-Verlet algorithm (which provides an *approximate* solution to Newton's equation of motion for arbitrary potentials).  That is,
the vibrational motion of a diatomic subject to a Harmonic potential predicted
by the Velocity-Verlet algorithm should closely match the analytical solution. Analytically,
the bond length as a function of time for a diatomic experiencing a harmonic potential is given by
\begin{equation}
r(t) = A \: {\rm sin}\left(\sqrt{\frac{k}{\mu}} t + \phi \right) + r_{eq},
\end{equation}
where $A = \frac{r(0)}{{\rm sin}(\phi)}$, $r(0)$ is the initial separation, and $\phi$ is the initial phase of the cycle; note that corresponding to this initial separation is
an initial velocity given by
\begin{equation}
v(0) = A \: \sqrt{\frac{k}{\mu}} {\rm cos}\left( \phi \right).
\end{equation}

Let's define a function harmonic_position that takes arguments of $\sqrt{\frac{k}{\mu}}$ (om), $A$ (amp), $\phi$ (phase), $r_{eq}$ (req), and time (t), and returns the separation.

```python
''' Students will write this! '''
def harmonic_position(om, Amp, phase, req, time):
    return  Amp * np.sin( om * time + phase ) + req

```

The following code block will call the Velocity Verlet algorithm using
the RHF Harmonic potential 10,000 times with a
timestep of 0.1 atomic units per timestep and will compare the resulting trajectory of bond length vs time (all in atomic units) to the analytic result for the Harmonic oscillator; we will initiate the bond length as being 0.2 atomic units **longer** than $r_eq$ with an initial phase of $\frac{\pi}{4}$.

```python
''' This will be pre-written! '''
### how many updates do you want to perform?
N_updates = 10000

### establish time-step for integration to be 0.02 atomic units... this is about 0.0005 femtoseconds
### so total time is 200000*0.02 atomic units of time which is ~9.6e-13 s, or 960 fs
dt = 0.1

### results from VV algorithm
hr_vs_t = np.zeros(N_updates)
hv_vs_t = np.zeros(N_updates)
### analytic result for r(t)
ar_vs_t = np.zeros(N_updates)
### array to store time in atomic units
t_array = np.zeros(N_updates)

### establish some constants relevant for analytic solution
### harmonic freq
om = np.sqrt(RHF_k/mu)
### initial displacement
x0 = 0.2
### amplitude for analytic solution
Amp = x0/(np.sin(np.pi/4))
### initial velocity
v0 = Amp * om * np.cos(np.pi/4)

hr_vs_t[0] = RHF_Req+x0
hv_vs_t[0] = v0

### We need a spline object for the harmonic force to pass to the Velocity Verlet algorithm,
### let's get that now!

### spline for Harmonic potential using RHF_k
RHF_Harm_Pot_Spline = InterpolatedUnivariateSpline(r_fine, RHF_Harm_Pot, k=3)
### RHF harmonic force
RHF_Harm_Force = RHF_Harm_Pot_Spline.derivative()

### first Velocity Verlet update
result_array = Velocity_Verlet(hr_vs_t[0], hv_vs_t[0], mu, RHF_Harm_Force, dt)
### first analytic result
ar_vs_t[0] = harmonic_position(om, Amp, np.pi/4, RHF_Req, 0)
### do the update N_update-1 more times
for i in range(1,N_updates):
    ### store current time
    t_array[i] = dt*i
    ### Compute VV update
    result_array = Velocity_Verlet(result_array[0], result_array[1], mu, RHF_Harm_Force, dt)
    ### store results from VV update
    hr_vs_t[i] = result_array[0]
    hv_vs_t[i] = result_array[1]
    ### compute and store results from analytic solution
    ar_vs_t[i] = harmonic_position(om, Amp, np.pi/4, RHF_Req, dt*i)

### Plot result and compare!
plt.plot(t_array, hr_vs_t, 'red', label="Velocity Verlet")
plt.plot(t_array, ar_vs_t, 'b--', label="Analytic")
plt.legend()
plt.show()

```

Now let's simulate the vibrational motion of HF subject to the *ab* *initio* forces we computed earlier and compare them to the Harmonic motion; recall we have already obtained spline objects for RHF, MP2, and CCSD forces called RHF_Force, MP2_Force, and CCSD_Force.
We will also initialize the simulations using the same values as we did with the Harmonic case to aid our comparison.

```python
""" Students will write this! """
### Now use r_init and v_init and run velocity verlet update N_updates times, plot results
### these arrays will store the time, the position vs time, and the velocity vs time
r_vs_t = np.zeros(N_updates)
v_vs_t = np.zeros(N_updates)

### first entry is the intial position and velocity
r_vs_t[0] = RHF_Req+x0
v_vs_t[0] = v0

### first Velocity Verlet update
result_array = Velocity_Verlet(r_vs_t[0], v_vs_t[0], mu, RHF_Force, dt)

for i in range(1,N_updates):
    result_array = Velocity_Verlet(result_array[0], result_array[1], mu, RHF_Force, dt)
    r_vs_t[i] = result_array[0]
    v_vs_t[i] = result_array[1]

plt.plot(t_array, r_vs_t, 'red', label='ab initio')
plt.plot(t_array, hr_vs_t, 'b--', label='Harmonic')
plt.legend()
plt.show()

```

#### How are the dynamics different when the *ab* *initio* forces are used?  Try to identify at least two quantitative ways in which you can distinguish the harmonic motion from the motion deriving from the *ab* *initio* forces.

#### Can you estimate the frequency from the *ab* *initio* trajectories?  How does this frequency compare with the Harmonic approximation and with the experimental value?

### For further consideration: What makes a "sensible range of values" for position and velocity?

In this case, we will initialize the position to be a random number between 1.0 and 4.0; for the velocity, we will use the fact that we can estimate the expectation value of kinetic energy for a very similar system (the Harmonic oscillator) in the ground state as follows:
\begin{equation}
\langle T \rangle = \frac{1}{2} E_g,
\end{equation}
where $E_g$ is the ground state of the Harmonic oscillator (this is making use of the Virial theorem).  We can easily
find the ground state energy in the Harmonic oscillator approximation of $HF$ using our frequency calculation described above as
\begin{equation}
E_g = \frac{1}{2} h \nu,
\end{equation}
which implies the kinetic energy expectation value is
\begin{equation}
\langle T \rangle = \frac{h}{8 \pi} \sqrt{\frac{k}{\mu}}.
\end{equation}
Since we can say classically that the kinetic energy is given by $T = \frac{1}{2}\mu v^2$, we can estimate the velocity of the bond stretch as follows:
\begin{equation}
v = \sqrt{\frac{2 \langle T \rangle}{\mu}} = \sqrt{ \frac{\hbar \sqrt{\frac{k}{\mu}}}{2\mu}}
\end{equation}
where we have simplified using the fact that $\hbar = \frac{h}{2\pi}$ ($\hbar$ has the value 1 in the atomic unit system we are using up to this point!).  We will assume that a reasonable
range of velocities spans plus or minus 3 times this "ground-state" velocity.

```python
### define "ground-state" velocity for each level of theory
v_RHF = np.sqrt( np.sqrt(RHF_k/mu)/(2*mu))
v_MP2 = np.sqrt( np.sqrt(MP2_k/mu)/(2*mu))
v_CCSD = np.sqrt( np.sqrt(CCSD_k/mu)/(2*mu))

### get random position and velocity for RHF HF within a reasonable range
#r_init = np.random.uniform(0.75*RHF_Req,2*RHF_Req)
r_init = RHF_Req
v_init = np.random.uniform(-2*v_RHF,2*v_RHF)

### print initial position and velocity
print("Initial separation is ",r_init, "atomic units")
print("Initial velocity is   ",v_init, "atomic units")

### get initial force on the particle based on its separation
RHF_F_init = -1*RHF_Force(r_init)
print("Initial Force is ", RHF_F_init, "atomic units")

```

    Initial separation is  1.6975235344966797 atomic units
    Initial velocity is    -0.0013839204810472618 atomic units
    Initial Force is  -0.0003233757202937039 atomic units

Now that we have our initial conditions chosen, our force as a function of separation known, and our Velocity Verlet function completed, we are ready to run our simulations!

```python

```


# Solutions to auto-diagnostic exercises 2
Exercises by L.C. Félix-Herrán

### 1)
Z-transform of a ramp
$$ f(t) = \begin{cases} At, & t \ge 0\\ 0, & t<0 \end{cases} $$

Sampling gives
$$ f(kh) = \begin{cases} (Ah)k, & k \ge 0\\ 0, & k<0 \end{cases} $$
Solution by using property
$$ \mathcal{Z}\{kg(k)\} = -z\frac{d}{dz} G(z) $$
and the pair
$$ g(kh) = Ah \qquad G(z) = Ah \frac{z}{z-1}. $$
$$ f(kh) = k g(kh), $$
so
\begin{align}
F(z) &= -z\frac{d}{dz} Ah \frac{z}{z-1} = -Ahz \left(\frac{1}{z-1} \frac{d}{dz} z + z \frac{d}{dz} \frac{1}{z-1} \right)\\
     &= -Ahz \left( \frac{1}{z-1} + z(-\frac{1}{(z-1)^2}) \right)\\
     &= -Ahz \frac{z-1 - z}{(z-1)^2} = A \frac{hz}{(z-1)^2}
 \end{align}

### 2)

Calculate the z-transform of the unit step advanced $5$, i.e.
$$ f(t) = u_s(t+5h) = \begin{cases} 1, & t \ge -5h\\0 ,& t<-5h. \end{cases} $$
\begin{align}
F(z) &= \sum_{k=0}^\infty u_s(kh+5h) z^{-k}, \quad \text{change of variables}\, k+5=l
&= \sum_{l=5}^\infty z^{-l+5}  = z^5 \sum_{l=5} z^{-l}\\
&= z^5 \left(\sum_{l=0}^\infty z^{-l} - \sum_{l=0}^4 z^{-k}\right)\\
&= z^5 \left( \frac{z}{z-1} - 1 - \frac{1}{z} - \frac{1}{z^2} - \frac{1}{z^3} - \frac{1}{z^4}\right)\\
&= \frac{z^5 - z^5 + z^4 - z^4 + z^3 - z^3 + z^2 - z^2 +z -1}{z^4(z-1)}\\
&= \frac{1}{z^4(z-1)}
\end{align}

### 3)

Final value theorem applied to $$ Y(z) = \frac{z+1}{z^2 - 0.7z + 0.1}. $$
\begin{align}
\lim_{k\to \infty} y(k) &= \lim_{z \to 1} (1-z^{-1})Y(z) = \lim_{z \to 1} \frac{z-1}{z} Y(z)\\
&= \lim_{z \to 1} \frac{z^2 - 1}{z(z^2 - 0.7z + 0.1)} = \frac{0}{0.4} = 0.
\end{align}

### 4)

Find z-transform of $f(k) = (\frac{1}{2})^k$.
\begin{align}
F(z) &= \sum_{k=0}^\infty f(k) z^{-k} = \sum_{k=0}^\infty \left(\frac{1}{2z}\right)^k\\
&= \frac{1}{1-\frac{1}{2z}} = \frac{z}{z-\frac{1}{2}}, \quad 2z>1 \, \text{Region of convergence}
\end{align}

### 5)

Starting value theorem applied to $$ F(z) = \frac{z^2 + 8z + 3}{3z^2 - 9z + 2}. $$
\begin{align}
f(0) &= \lim_{z\to \infty} F(z) = \lim_{z\to \infty} \frac{z^2 + 8z + 3}{3z^2  - 9z + 2}\\
&= \lim_{z\to \infty} \frac{1 + 8z^{-1} + 3z^{-2}}{3 - 9z^{-1} + 3z^{-2}} = \frac{1}{3}.
\end{align}

### 6)

Find the z-transform of the sequence $$0,0.25, 0.5, 0.75, 1, 1, \ldots$$

```python
import numpy as np
import matplotlib.pyplot as plt
k = np.arange(0,8)
plt.figure(figsize=(12,1))
plt.stem(k, [0,0.25,0.5,0.75,1,1,1,1])
plt.xticks(k)
plt.xlabel('$k$')
plt.ylabel('$f(k)$');
```

Write the sequence as $$f(k) = u_s(k) - \delta(k) - 0.75\delta(k-1) - 0.5\delta(k-2) -0.75\delta(k-3).$$
\begin{align}
F(z) &= \frac{z}{z-1} - 1 - 0.75z^{-1} - 0.5z^{-2} - 0.25z^{-3}\\
&= \frac{z^4 - z^3(z-1) - 0.75z^2(z-1) - 0.5z(z-1) - 0.25(z-1)}{(z-1)z^3}\\
&=  \frac{0.25(z^3+z^2+z+1)}{(z-1)z^4} \cdot \frac{z-1}{z-1}\\
&= \frac{z^4 - z^3 + z^3 - z^2 + z^2 - z + z -1}{4z^4(z-1)^2}\\
&= \frac{z^4-1}{4z^4(z-1)^2}
\end{align}

### 7)
Find the z-transform of $f(t) = \mathrm{e}^{-\beta t)\cos(4t)$. Use the complex translation theorem:
$$ \mathcal{Z} \{ \mathrm{e}^{-\alpha k} x(k) \} = X(z\mathrm{e}^{\alpha}). $$
Proof of the theorem:

\begin{align}
\mathcal{Z} \{ \mathrm{e}^{-\alpha k} x(k) \} &= \sum_{k=0}^\infty (\mathrm{e}^{\alpha})^{-k} x(k) z^{-k}, \quad \text{change of variables}\, y=\mathrm{e}^{\alpha}z\\
&= \sum_{k=0}^{\infty} x(k) y^{-k} = X(y) = X(\mathrm{e}^\alpha z).
\end{align}

Here we have $f(kh) = \mathrm{e}^{-\beta h k}\cos(4kh)$, so $\alpha = \beta h$. Also
$$\mathcal{Z} \{ \cos(4kh) \} = \frac{z^2 - z\cos(4h)}{z^2 - 2z\cos(4h) + 1}.$$
We get, with $z \rightarrow z\mathrm{e}^{\beta h}$
\begin{align}
\mathcal{Z} \{ f(kh) \} &=  \frac{(z\mathrm{\beta h})^2 - (z\mathrm{\beta h})\cos(4h)}{(z\mathrm{\beta h})^2 - 2(z\mathrm{\beta h})\cos(4h) + 1}\\
&= \frac{z^2 - z\mathrm{e}^{-\beta h}\cos(4h)}{z^2 - 2\mathrm{e}^{-\beta h}\cos(4h) z + \mathrm{e}^{-2\beta h}}.
\end{align}

### 8)
Find the z-transform of $f(t) = \cos(\omega_1 t)$, using Euler's formula.
$$ \cos\omega_1 kh = \frac{1}{2}\mathrm{e}^{i\omega_1 kh} + \frac{1}{2} \mathrm{e}^{-i\omega_1kh}.$$
Since
$$ \mathcal{Z} \{ \mathrm{e} ^{i\omega_1 kh}\} = \sum_{k=0}^\infty (\mathrm{e}^{i\omega_1 h})^kz^{-k} = \frac{z}{z-\mathrm{e}^{i\omega h}}, $$
\begin{align}
F(z) &= \frac{1}{2} \left( \frac{z}{z-\mathrm{e}^{i\omega_1 h}} + \frac{z}{z-\mathrm{e}^{-\omega_1 h}} \right)\\
&= \frac{\frac{1}{2}z( z-\mathrm{e}^{-i\omega_1 h} + z - \mathrm{e}^{i\omega_1 h})}{(z-\mathrm{e}^{i\omega_1h})(z - \mathrm{e}^{-i\omega_1 h})}\\
&= \frac{z^2 - z\cos(\omega_1 h)}{z^2 - 2\cos(\omega_1 h)z + 1}
\end{align}

### 9)
Apply the final-value theorem and the initial value theorem to $$F(z) = \frac{z+1}{z^2 + 8z + 2}$$.
#### Final value
\begin{align}
\lim_{k \to\infty} f(k) &= \lim_{z \to 1} (1-z^{-1})F(z) = \lim_{z\to 1} \frac{(1-z^{-1})(z+1)}{z^2 + 8z + 2}\\
&= \lim_{z \to 1} \frac{z+1-1-z^{-1}}{z^2 + 8z + 2} = 0
\end{align}
#### Initial value
\begin{align}
f(0) &= \lim_{z \to \infty} F(z) = \lim_{z\to \infty} \frac{z^{-1} + z^{-2}}{1 + 8z^{-1} + 2z^{-2}} = 0
\end{align}

### 10)
Find the discrete-time signal $f(k)$ corresponding to the z-transform $$F(z) = \frac{4z+8}{(4z^2 + 8z + 4)(z+2)^2}.$$
Use partial fraction expansion, but first note that $F(z)$ can be simplified
$$ F(z) = \frac{4(z+2)}{(4z^2 + 8z + 4)(z+2)^2} = \frac{4}{4(z+1)^2(z+2)} = \frac{1}{(z+1)^2(z+2)} = \frac{1}{z+2} - \frac{1}{z+1} + \frac{1}{(z+1)^2}$$

So, we get
$$ F(z) = z^{-1}G(z) = z^{-1} \left(  \frac{1}{z+2} - \frac{1}{z+1} + \frac{1}{(z+1)^2} \right), $$
with $f(k) = g(k-1)$ and
$$ g(k) = \left((-2)^{k} - (-1)^{k} + k(-1)^{k+1}\right) u_s(k) $$

\begin{align}
f(k) &= \left( (-2)^{k-1} - (-1)^{k-1} + (k-1)(-1)^{k} \right) u_s(k-1)\\
&= \left( (-2)^{k}\frac{1}{-2} - (-1)^{k}(-1) + k(-1)^{k}(-1) - (-1)^{k} \right) u_s(k-1)\\
 &= \left(-\frac{1}{2}(-2)^{k} - k(-1)^{k}\right) u_s(k-1)
 = \left(\frac{1}{2}\delta(k) -\frac{1}{2}(-2)^{k} - k(-1)^{k}\right) u_s(k)
\end{align}

### 11) and 12) Similar approach as 10)
Use [Wolfram Alpha](https://www.wolframalpha.com/input/?i=inverse%20Z%20transform%20calculator) to check your calculations.

```python

```


Notebook version 1.0, 31 Aug 2021. Written by Otto Salmenkivi / CSC - IT Center for Science Ltd. otto.salmenkivi@gmail.com

Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php

Tested on Kvasi, running QLM version 1.2.1: https://research.csc.fi/-/kvasi
***
# Quantum Algorithm for calculating the expected value from a probability distribution

In this notebook we employ the [Quantum Amplitude Estimation][1] (QAE) algorithm to estimate the expected value of a probability distribution. The QAE algorithm can theoretically provide a quadratic speed-up compared to similar classical calculations. The algorithm consist of the following parts:
- Distribution loading with $D$ gate to `nb_dist_qbits` number of qubits
- $F$ gate representing the objective function
- A Quantum Phase Estimation protocol that utilizes `nb_eval_qbits` number of qubits

We have a discrete variable $i \in \{0,1,2,3\}$, that we map to quantum states $|00\rangle$, $|01\rangle$, $|10\rangle$ and $|11\rangle$. The values and therefore the states have probabilities $\frac{1}{2}$, $\frac{1}{4}$, $\frac{1}{6}$ and $\frac{1}{12}$, respectively. We can easily calculate the expected value:
\begin{equation}
    \mathbb{E}[i] = \sum_i p_i\cdot i = \dfrac{1}{2}\cdot 0 + \dfrac{1}{4} \cdot 1 + \dfrac{1}{6} \cdot 2 + \dfrac{1}{12} \cdot 3 \approx 0,83\,.
\end{equation}
Our task is to approximate the value above with a quantum algorithm.

[1]: https://arxiv.org/abs/quant-ph/0005055

We begin by importing the libraries needed

```python
from qat.lang.AQASM import Program, QRoutine, AbstractGate, H, SWAP
from qat.lang.AQASM.qftarith import QFT
from qat.qpus import LinAlg
import numpy as np
import matplotlib.pyplot as plt
```

```python
# Defining number of qubits used in the algorithm
nb_eval_qbits = 6
nb_dist_qbits= 2 # Cannot be changed because Distribution is hand-build for 2 qubits

# Initializing abstract Distribution, F and Q gates
D_gate = AbstractGate("D", [], arity = nb_dist_qbits)
F_gate = AbstractGate("F", [], arity = nb_dist_qbits + 1)
Q_gate = AbstractGate("Q", [int], arity = nb_dist_qbits + 1)

# Constants
c = 1/2    # c is a constant used in a linear approximation in the QAE algoritm, defined within (0,1]
```

## D gate: Loading the uncertainty distribution to 2 qubits with parametrized RY gate matrices
The integers $i \in \{0,1,2,3\}$ are mapped to a register of two qubits so that $i = 2i_0 + i_1$. Indexing follows the QLM convention.

We create matrices corresponding to the $R_y$ gates and calculate the resulting matrix, we call it $D$ gate. The values for the rotation angles were chosen to correspond with probabilities mentioned earlier.

```python
# Matrix of the standard RY gate
def ry_matrix(theta):
    return np.array([[np.cos(theta/2),-np.sin(theta/2)],[np.sin(theta/2),np.cos(theta/2)]])

# The resulting matrix of two RY gates acting on q_0 and q_1 respectively
def D_matrix():
    theta0 = 2*np.arccos(np.sqrt(2/3))
    theta1 = np.pi/3
    matrix = np.kron(ry_matrix(theta1), ry_matrix(theta0))
    return matrix

# Attach matrix to gate
D_gate.set_matrix_generator(D_matrix)
```

## F gate
Next task is to build the matrix for the $F$ gate that acts on the objective qubit controlled by the distribution qubits.  The key is to flip the objective qubit with parametrised $R_y$-gates. The goal is to change the probability of measuring the objective qubit in state $|1\rangle$, let's call it $a$.

Because we are approximating the expected value of the variable itself, we define our objective function as $f(i) = i$. For the F operator though, we need to normalise it to $[0,1]$, so we get $\tilde{f}(i) = \dfrac{i}{3}$.

For more information on building the circuit for the objective function, see [Stamatopoulus et al. 2020][1]. Here are the highlights:

With the $F$ operator we want to create the following tranformation:
\begin{equation}
    |i_0i_1\rangle|0\rangle \rightarrow |i_0i_1\rangle\big(\cos[f(i)]|0\rangle + \sin[f(i)]|1\rangle\big).
\end{equation}
Register $|i_0i_1\rangle$ is used for the distribution and the objective qubit is prepared in the zero state. Instead of using $\tilde{f}(i)$, we need to set the function as
\begin{equation}
    f(i) = c\left(2\cdot\dfrac{i}{3}-1 \right) + \dfrac{\pi}{4}\,,
\end{equation}
where $c \in (0,1]$ is a constant used in a linear approximation. By using $i = 2i_0 + i_1$, we get
\begin{equation}
    \dfrac{4c}{3}i_0 + \dfrac{2c}{3}i_1 - c + \dfrac{\pi}{4} \,.
\end{equation}
The operator is constructed from $R_y$ gates. The constant term is achieved by operating with $R_y\left(-c + \frac{\pi}{4}\right)$ on the objective qubits. The linear terms are implemented with controlled gates. A $R_y\left(\frac{4c}{3}\right)$ gate, controlled by $i_0$, and a $R_y\left(\frac{2c}{3}\right)$ gate, controlled by $i_1$, are applied on the objective.

The reason for this procedure is that with the probability to measure the last qubit in state $|1\rangle$ is related to the expected value of the objective function. We have
\begin{align}
    a &= \mathbb{E}[f(i)] = \mathbb{E}\left[c\left(2\cdot\dfrac{i}{3}-1 \right) + \dfrac{\pi}{4}\right]\\
    \Leftrightarrow \quad \mathbb{E}[i] &= \dfrac{3}{2}\left(\dfrac{a-\frac{1}{2}}{c} + 1 \right)\,.
\end{align}

We could easily build the $F$ operator with individual gates, but we choose to use matrix definitions and the `AbstractGate` class instead. This is because we need to have the matrix representation when building the $A$ and $Q$ gates in the following sections

[1]: https://arxiv.org/pdf/1905.02666.pdf

```python
# A 8x8 matrix that correspond to identity on first two qubist and a Ry on the last
def only_ry(theta):
    return np.kron(np.identity(4),ry_matrix(2*theta)) # The angle is doubled due to different definitions on RY matrix

# For the second gate matrix we need a matrix representation of a controlled RY,
# where the qubits are not adjecent
def cry_0to2(theta):
    theta = 2*theta # Again, doubled due to different matrix definitions
    matrix = np.array([[1,0,0,0,0,0,0,0],
                       [0,1,0,0,0,0,0,0],
                       [0,0,1,0,0,0,0,0],
                       [0,0,0,1,0,0,0,0],
                       [0,0,0,0,np.cos(theta/2),-np.sin(theta/2),0,0],
                       [0,0,0,0,np.sin(theta/2),np.cos(theta/2),0,0],
                       [0,0,0,0,0,0,np.cos(theta/2),-np.sin(theta/2)],
                       [0,0,0,0,0,0,np.sin(theta/2),np.cos(theta/2)]])
    return matrix

# controlled RY when most significant is control
def cry_1to2(theta):
    theta = 2*theta # Again, doubled due to different matrix definitions
    cry_matrix= np.array([[1,0,0,0],
                          [0,1,0,0],
                          [0,0,np.cos(theta/2),-np.sin(theta/2)],
                          [0,0,np.sin(theta/2),np.cos(theta/2)]])
    matrix = np.kron(np.identity(2),cry_matrix)
    return matrix

# combining all three
def F_matrix():
    f_1 = 2*c/3
    f_0 = - c + np.pi/4
    matrix = np.matmul(cry_0to2(2*f_1),np.matmul(cry_1to2(f_1),only_ry(f_0)))
    return matrix

# attach to gate
F_gate.set_matrix_generator(F_matrix)
```

## A gate
Before the $Q$ gates, we need calculate the matrix $A$, which is the effective operator of both $D$ and $F$ gate on the objective and the distribution qubits. That is
\begin{equation}
    A|q_{0}q_{1}q_{obj}\rangle = F \cdot (D \otimes I)|q_0q_1q_{obj}\rangle\,.
\end{equation}

```python
def make_A_matrix():
    return np.matmul(F_matrix(),np.kron(D_matrix(),np.identity(2)))
```

To get the statevector $|\psi\rangle$ needed in the contruction of $Q$, we can run the $A$ gate separately an measure only the distribution qubits.

```python
# Run a program with only A gate
sv_prog = Program()
sv_qbits = sv_prog.qalloc(nb_dist_qbits+1)
A_gate = AbstractGate("A",[],arity = nb_dist_qbits+1,matrix_generator = make_A_matrix)
sv_prog.apply(A_gate(),sv_qbits)
A_result = LinAlg().submit(sv_prog.to_circ().to_job())

# get statevector from results
sv = A_result.statevector
print(f'Whole statevector after A gate:\n{sv}')

# Extract state correponding with last qubit states 0 and 1
psi_0 = []
psi_1 = []
for i in range(2**(nb_dist_qbits)):
    psi_0.append(sv[2*i])
    psi_1.append(sv[2*i+1])

a = sum([abs(i)**2 for i in psi_1])
print('The probability to measure the last qubit in state 1 based in the statevector is ',a)

print('Full measurement results after A gate:')
plt.figure()
states = [str(sample.state) for sample in A_result]
probs = [sample.probability for sample in A_result]
plt.bar(states,probs)
plt.ylabel('Probability')
plt.xticks(rotation = 60)
plt.draw()
```

At this point, we can already check the results from our quantum circuit, because we have a value for $a$, which we can trace back to the expected value.

This can be considered cheating, because this method does not provide a computatinal speed up. The point of the QAE algorithm is to get an approximation in a way that the approximation error converges quadratically faster than just by repeating the measument for the $A$ gate. But anyway, since our circuits are so small, we can use this cheat as a comparison.

```python
# Calculating Expected value based on value a that we can recover in the previous cell
def ExpectedFx(a):
    return 3*((a-1/2)/c+1)/2

print('Expected value for V from repeated A gates:',ExpectedFx(a),'.\n'
     'It is quite close to the true value.')

```

## The QPE and the Q gates

Quantum Phase estimation is well used tool in the world of quantum algorithms. For it we need additional evaluation qubits, the $Q$ gate and an inverse Quantum Fourier Transform. In addition, we need to flip the register before the inverse QFT.

The $Q$ gate is defined as $Q = AS_0A^{\dagger}S_{\psi_0}$, where $S_0 = I-2|0\rangle_{n+1}\langle0|_{n+1}$ and $S_{\psi_0} = I-2|\psi_0\rangle_n|0\rangle\langle\psi_0|_n\langle0|$ are reflections in the statevector space to amplify the "good" states, defined by the last qubit in state $|1\rangle$. Different powers of $Q^{2^j}$ gates are applied to the distribution and objective registers, when $j$ is the index of the control qubit in the evaluation register (reversed in the QLM convention).

```python
# Creating a matrix representation of Q based on Woerner & Egger 2018 Appendix A
def Q_matrix(j):
    A_matrix = make_A_matrix()
    statevector_psi = psi_0

    # We renormalize psi_0
    n = len(statevector_psi)
    statevector_sum = sum([abs(i)**2 for i in statevector_psi])
    for i in range(n):
        statevector_psi[i]  = statevector_psi[i]/np.sqrt(statevector_sum)

    # Preparing the needed states |0> and <0| as numpy arrays
    ket_zero = np.array([[1],[0]],dtype=complex)
    bra_zero = np.conjugate(np.transpose(ket_zero))

    #Preparing states |0>_n and <0|_n
    ket_zero_n = np.zeros((2*n,1),dtype= complex)
    ket_zero_n[0][0] = 1
    bra_zero_n = np.conjugate(np.transpose(ket_zero_n))

    # States |psi> and <psi| as numpy array based on input statevector
    ket_psi = np.zeros((n,1), dtype=complex)
    for i,amp in enumerate(statevector_psi):
        ket_psi[i][0] = amp

    bra_psi = np.conjugate(np.transpose(ket_psi))

    # Calculating the parentheses on the Q operator equation
    I = np.identity(2*n)
    first_parenthesis = I-2*np.matmul(ket_zero_n,bra_zero_n)
    second_parenthesis = I-2*np.matmul(np.kron(ket_psi,ket_zero),np.kron(bra_psi,bra_zero))

    # The Hermitian conjugate of operator A
    A_matrix_dag = np.conjugate(np.transpose(A_matrix))

    # Calculating the power based on parameter j
    single_matrix = np.matmul(A_matrix,np.matmul(first_parenthesis,np.matmul(A_matrix_dag,second_parenthesis)))
    Q_matrix = single_matrix
    i = 0
    while i < 2**j-1:
        Q_matrix = np.matmul(single_matrix,Q_matrix)
        i += 1
    return Q_matrix

# Attach to gate
Q_gate.set_matrix_generator(Q_matrix)
```

```python
# Do SWAPS
def swap_register(m):
    rout = QRoutine()
    i = 1
    while i <= m/2:
        rout.apply(SWAP, i-1, m-i)
        i += 1
    return rout
```

```python
# function to apply Hadamard gates multiple qubits
def hadamard_all(nbqbits):
    rout = QRoutine()
    for i in range(nbqbits):
        rout.apply(H,i)
    return rout
```

## It's time to build the whole circuit.
We have defined all the pieces we need, so let's build the circuit.

```python
def create_the_routine(nb_eval_qbits,nb_dist_qbits):
    rout = QRoutine()
    # Distribution qubits
    dist_qbits = rout.new_wires(nb_dist_qbits)
    # The objective qbit
    obj_qbit = rout.new_wires(1)
    # Evaluation qubits
    eval_qbits = rout.new_wires(nb_eval_qbits)
    # Hadamards to evaluation qubits
    rout.apply(hadamard_all(nb_eval_qbits),eval_qbits)
    # D gate to distribution qubits based on distribution parameters
    rout.apply(D_gate(), dist_qbits)
    # F gate to objective and distribution qubits
    rout.apply(F_gate(),dist_qbits, obj_qbit)
    # A range of Q qates
    for j in range(nb_eval_qbits):
        rout.apply(Q_gate(j).ctrl(), eval_qbits[nb_eval_qbits-1-j], dist_qbits, obj_qbit)
    # Flip the register
    if nb_eval_qbits > 1 : rout.apply(swap_register(nb_eval_qbits), eval_qbits)
    # Apply inverse QFT
    rout.apply(QFT(nb_eval_qbits).dag(),eval_qbits)
    return rout
```

```python
prog = Program()
qbits = prog.qalloc(nb_eval_qbits+nb_dist_qbits+1)
prog.apply(create_the_routine(nb_eval_qbits,nb_dist_qbits),qbits)
circuit = prog.to_circ()
%qatdisplay circuit
```

Let's print the results from simulating the circuit above with a LinAlg simulator.

```python
result = LinAlg().submit(circuit.to_job(nbshots = 0, qubits = range(3,3+nb_eval_qbits)))
for sample in result:
    print(f'State: {sample.state},  amplitude: {sample.amplitude},  probability: {sample.probability}')

# Visualizing the initial measurement results
import matplotlib.pyplot as plt
plt.figure()
states = [str(sample.state) for sample in result]
probs = [sample.probability for sample in result]
plt.bar(states,probs)
plt.ylabel('Probability')
plt.xlabel('States')
plt.xticks(rotation = 60)
plt.draw()
```

## Post-prosessing the measurement results

The measurent gives integers $y$ which need to be mapped to an estimator $\tilde{a} = \sin^2(y\pi/M) \in [0,1]$.

```python
# Creating an array of zeros for all state probabilities and replacing the non-zero values for measured probabilities
all_probs = np.zeros(2**nb_eval_qbits, dtype=float)
for sample in result:
    state_decimal = sample.state.int
    all_probs[state_decimal] = sample.probability

# The mapping used between measured states and corresponding estimate for the probability p
a_tildes = [np.sin(i*np.pi/(2**nb_eval_qbits))**2 for i in range(2**(nb_eval_qbits-1)+1)]

# Aggregating the data from different states that correspond to same probability bins
probs =[]
probs.append(all_probs[0])
i = 1
while i < 2**nb_eval_qbits/2:
    #print(f'These states correspond to the same bin: {i} and {2**nb_eval_qbits-i}')
    probs.append(all_probs[i] + all_probs[2**nb_eval_qbits-i])
    i += 1
probs.append(all_probs[2**(nb_eval_qbits-1)])

# finding a with highest probability
lucky_a_tilde = a_tildes[np.argmax(probs)]
print(f'The algorithm gives an estimate a = {lucky_a_tilde}')
```

We can visualise the end results by plotting the discrete probability distribution of $\tilde{a}$. We can also display the same distribution as a function of $\mathbb{E}[i]$ and show the true value (red line).

```python
plt.figure()
plt.bar(a_tildes, probs, width = 0.025)
plt.ylabel('Probability')
plt.xlabel('Estimator for a')
plt.plot()

plt.figure()
plt.bar([ExpectedFx(a_tilde) for a_tilde in a_tildes], probs, width = 0.05)
plt.ylabel('Probability')
plt.xlabel(r'E[i]')
plt.axvline(x=0.8333, color = 'r', linestyle = '--', linewidth = 2 )
plt.xlim(0,4)
plt.plot()

print(f'The best approximation for expected value for i = {ExpectedFx(lucky_a_tilde)}')
```

## The end
And there you have it!  We started from a probability distribution for a discrete variable and calculated the expected value for it using the QAE algorithm. Next step could be to approximate some linear function based on the variable, and mayde make it more interesting by using multiple variables each with a different probability distributions.


# Wave Equation
## The Differential Equation
Condsider the one-dimensional hyperbolic Wave Equation:
\begin{equation}  \frac{\partial u}{\partial t} +a\frac{\partial u}{\partial x}=0,\end{equation}
with the initial conditions
\begin{equation} u(x,0)=1-\cos(x), \ \ 0 \leq x \leq 2\pi. \end{equation}
and wrap around boundary conditions.

This notebook will implement the Lax-Friedrich method to appoximate the solution of the Wave Equation.
The Lax-Fredrich method was designed by Peter Lax (https://en.wikipedia.org/wiki/Peter_Lax) and Kurt Otto Friedrichs (https://en.wikipedia.org/wiki/Kurt_Otto_Friedrichs).

```python
# LIBRARY
# vector manipulation
import numpy as np
# math functions
import math

# THIS IS FOR PLOTTING
%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import warnings
warnings.filterwarnings("ignore")
```

## Discete Grid
The region $\Omega$ is discretised into a uniform mesh $\Omega_h$. In the space $x$ direction into $N$ steps giving a stepsize of
\begin{equation} h=\frac{1-0}{N},\end{equation}
resulting in
\begin{equation}x[i]=0+ih, \ \ \  i=0,1,...,N,\end{equation}
and into $N_t$ steps in the time $t$ direction giving a stepsize of
\begin{equation} k=\frac{1-0}{N_t}\end{equation}
resulting in
\begin{equation}t[i]=0+ik, \ \ \ k=0,...,K.\end{equation}
The Figure below shows the discrete grid points for $N=10$ and $Nt=100$,  the known boundary conditions (green), initial conditions (blue) and the unknown values (red) of the Heat Equation.

```python
N=20
Nt=10
h=2*np.pi/N
k=1/Nt
r=k/(h*h)
time_steps=10
time=np.arange(0,(time_steps+.5)*k,k)
x=np.arange(0,2*np.pi+h/2,h)

X, Y = np.meshgrid(x, time)

fig = plt.figure()
plt.plot(X,Y,'ro');
plt.plot(x,0*x,'bo',label='Initial Condition');
plt.xlim((-h,2*np.pi+h))
plt.ylim((-k,max(time)+k))
plt.xlabel('x')
plt.ylabel('time (ms)')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.title(r'Discrete Grid $\Omega_h$ ',fontsize=24,y=1.08)
plt.show();
```

## Initial Conditions

The discrete initial conditions is,
\begin{equation} w[i,0]=1-\cos(x[i]), \ \ 0 \leq x[i] \leq \pi, \end{equation}
The Figure below plots values of $w[i,0]$ for the inital (blue) conditions for $t[0]=0.$
## Boundary Conditions
To account for the wrap-around boundary condtions
\begin{equation}w_{-1,j}=w_{N,j},\end{equation}
and
\begin{equation}w_{N+1,j}=w_{0,j}.\end{equation}

```python
w=np.zeros((N+1,time_steps+1))
b=np.zeros(N-1)
# Initial Condition
for i in range (0,N+1):
    w[i,0]=1-np.cos(x[i])

fig = plt.figure(figsize=(8,4))
plt.plot(x,w[:,0],'o:',label='Initial Condition')
plt.xlim([-0.1,max(x)+h])
plt.title('Intitial Condition',fontsize=24)
plt.xlabel('x')
plt.ylabel('w')
plt.legend(loc='best')
plt.show()
ipos = np.zeros(N+1)
ineg = np.zeros(N+1)
for i in range(0,N+1):
   ineg[i] = i-1
   ipos[i] = i+1

ipos[N] = 0
ineg[0] = N

```

## Lax-Friedrichs Method
The Lax-Friedrichs Method of the Wave Equation is,
\begin{equation}
\frac{w_{ij+1}-\frac{w_{ij+1}+w_{ij-1}}{2}}{k}+a\big(\frac{w_{i+1j}-w_{i-1j}}{2h}\big)=0.
\end{equation}
Rearranging the equation we get,
\begin{equation}
w_{ij+1}=\frac{w_{ij+1}+w_{ij-1}}{2}-\frac{\lambda}{2} a(w_{i+1j}-w_{i-1j}),
\end{equation}
for $i=0,...10$ where $\lambda=\frac{k}{h}$.

This gives the formula for the unknown term $w_{ij+1}$ at the $(ij+1)$ mesh points
in terms of $x[i]$ along the jth time row.

```python
lamba=k/h
for j in range(0,time_steps):
    for i in range (0,N+1):
        w[i,j+1]=(w[int(ipos[i]),j]+w[int(ineg[i]),j])/2-lamba/2*(w[int(ipos[i]),j]-w[int(ineg[i]),j])

```

## Results

```python
fig = plt.figure(figsize=(12,6))

plt.subplot(121)
for j in range (1,time_steps+1):
    plt.plot(x,w[:,j],'o:')
plt.xlabel('x')
plt.ylabel('w')

plt.subplot(122)
plt.imshow(w.transpose(), aspect='auto')
plt.xticks(np.arange(len(x)), np.round(x,3),rotation=45)
plt.yticks(np.arange(len(time)), np.round(time,2))
plt.xlabel('x')
plt.ylabel('time')
clb=plt.colorbar()
clb.set_label('Temperature (w)')
plt.suptitle('Numerical Solution of the  Wave Equation'%(np.round(r,3)),fontsize=24,y=1.08)
fig.tight_layout()
plt.show()
```

```python

```

```python

```

Chapter 10 - Hyperbolic Equations/1002_Wave Equation-Lax-Friedrich.ipynb