Contents

1  Part 1; Final Exam CHEM 3PA3; Winter 2021

1.1  ✍ Write the Time-Dependent Schrödinger equation for one particle, in three dimensions, confined by the time-dependent potential $V(x,y,z,t)$.

1.2  ✍ Write the Time-Independent Schrödinger equation for a one-electron atom with atomic number $Z$ in atomic units.

1.3  🖩 Compute properties of a photon with wavelength 486.1 nm.

1.4  🧮 How many quantum numbers are needed to label the eigenstates of the following Hamiltonian:

1.5  🔀Below is a list of terms in the molecular Hamiltonian, together with their mathematical expressions.

1.6  🔀Below are lists of different Hamiltonians we considered in the course, together with the names of functions that are related to their solutions.

1.7  🖩 What is the zero-point energy for an electron confined to a 2-dimensional circular disk of radius $4 \text{ a.u.}$:

1.8  ✍ Write a Slater determinant for the excited-state, $1\text{s}^2 2\text{s}^1 3\text{s}^1$ configuration of the Beryllium atom.

1.9  🧮 Compute properties of a ${}^{7}\text{F}$ state.

1.10  ✍ Solve the Schrödinger equation for a muon, $\mu^-$, bound to Neon nucleus.

1.11  ✍ Write an expression for the lowest-energy molecular orbital of $\text{H}_3^{+2}$.

1.12  The next two questions refer to the Laplacian operator.

1.12.1  ✍ Show that $\nabla^2$ is a linear, Hermitian operator.

1.12.2  ✍ Show that $\nabla^2$ is negative definite.

1.13  Expectation values of $r^k$ and related properties in Hydrogenic atoms.

1.13.1  ✍ Expectation value of $r^{-1}$ for Hydrogenic Atoms.

1.13.2  🖩 Expectation value of $r^2$ for a 4f orbital in the hydrogen atom.

1.13.3  🖩 What is the most probable distance to find an electron from the nucleus in the 4f orbital of a hydrogen atom?

1.13.4  🖩 Heisenberg Uncertainty Principle

1.13.5  💰 Bonus: Derive an expression for $\sigma_r^2 \sigma_p^2$ for any state of a Hydrogenic atom with $l=n-1$.

1.14  Eigenfunctions and Eigenvalues for a two-dimensional Quantum Dot.

1.14.1  ✍ Write expressions for the eigenvalues and eigenfunctions of this system.

1.14.2  🖩 What is the wavelength that corresponds to the lowest-energy excitation when $a_x = 16$, $a_y = 9$, and $k_z = 4$?

1.15  ✍ What is the form of the eigenfunctions for the Kratzer-Fues Potential

1.15.1  💰 Bonus: Write an expression for the energy eigenvalues and the radial eigenfunctions of the Kratzer-Fues potential. This isn't 100% trivial, but try to see how far you can get.

# Part 1; Final Exam CHEM 3PA3; Winter 2021

👨🏫 **Overview:**
This is Part 1 of 3 Parts of your Final Exam/Project.
- Part 1. Questions on the Course Material. (40%)
- Part 2. Applying the Course material. (40%)
- Part 3. Discussing your exam and the Course Material. (20%)

**You must submit your Jupyter notebooks for Part 1 and Part 2 at least 48 hours prior to your appointment for Part 3.** You will be given your grade on Part 1 and Part 2 before the oral exam, so that you know what your status is. *For late submission of Part 1 and/or Part 2, I will deduct 2 points per hour.

📜 **Instructions:**
Answer the following questions. These are worth 40% of your exam, and each question is worth 2 points. (Some questions have multiple parts.) There is also a bonus problem.
- You can upload files for mathematical answers or type them in Markdown.
- You can use the notebook as a calculator for numerical problems; but you can also just type in your answer computed offline.
- You may find these [sheets containing reference data and mathematical formulas/identities](https://github.com/PaulWAyers/IntroQChem/blob/main/documents/ReferenceConstantsConversionsMath.pdf?raw=true) useful.
- You may also find it useful to know that the [speed of light, in atomic units](https://en.wikipedia.org/wiki/Hartree_atomic_units), is 137.036 a.u..

📖 **Rules for "Open Book" Exam:**
**Like all other portions of this exam, this part of the exam is open notes and open library. It is "open internet search" but you (obviously!) can't post questions on an internet discussion board or homework/problem/exam help site. You are not allowed to communicate with your classmates or any other human being (except me) about these questions or your responses, and this includes human beings (singular or plural, known or anonymous) online.**

☑ **Checklist Before Submission:**
- Make sure you fill in any place that says YOUR CODE HERE or "YOUR ANSWER HERE".
- Remove "Not Implemented Error" where appropriate.
- **Please put your name, username (the prefix to your @mcmaster.ca e-mail), and student ID number in the cell below.**
- Before you turn these problems in, make sure everything runs as expected.
  - First, restart the kernel (in the menubar, select Kernel → Restart).
  - Then run all cells (in the menubar, select Cell → Run All).
- Turn in your exam as a Jupyter notebook.

```python
Name = "First M. Last"
email_user_name = "username"
ID_number = 1234567

# It's useful to import these libraries.
# You can import others or not even use these, though.
import numpy as np
import scipy
from scipy import constants
```

## ✍ Write the Time-Dependent Schrödinger equation for one particle, in three dimensions, confined by the time-dependent potential $V(x,y,z,t)$.
**Note:** Write out the specific Hamiltonian; do not just write $\hat{H}$. Report your answer in the below markdown cell. You can drag-and-drop a photo/screenshot/image of your answer to the cell, but before submitting please confirm that the image is appropriately embedded.

=== BEGIN MARK SCHEME ===

The time-dependent Schrödinger equation is:
$$
\hat{H} \Psi(x,y,z,t) = i \hbar \frac{d \Psi(x,y,z,t)}{dt}
$$
Substituting in the specific value of the Hamiltonian,
$$
\left(-\frac{\hbar^2}{2m}\frac{d^2}{dx^2}-\frac{\hbar^2}{2m}\frac{d^2}{dy^2}-\frac{\hbar^2}{2m}\frac{d^2}{dz^2} + V(x,y,z,t) \right) \Psi(x,y,z,t) = i \hbar \frac{d \Psi(x,y,z,t)}{dt}
$$

=== END MARK SCHEME ===

## ✍ Write the Time-Independent Schrödinger equation for a one-electron atom with atomic number $Z$ in atomic units.
**Note:** Report your answer in the below Markdown Cell.

=== BEGIN MARK SCHEME ===

$$
\left(-\tfrac{1}{2} \nabla^2 - \tfrac{Z}{r} \right)\Psi(r,\theta,\phi) = \Psi(r,\theta,\phi)
$$
There are several other ways you could have written this. For example, you could have written out the Laplacian in spherical coordinates or in Cartesian coordinates. For example,
$$
\left(-\frac{1}{2}\frac{d^2}{dx^2} -\frac{1}{2}\frac{d^2}{dy^2}-\frac{1}{2}\frac{d^2}{dz^2}- \frac{Z}{\sqrt{x^2+y^2+z^2}} \right) \Psi(x,y,z) = \Psi(x,y,z)
$$

=== END MARK SCHEME ===

## 🖩 Compute properties of a photon with wavelength 486.1 nm.
The photon emitted when a 4p electron in the Hydrogen atom deexcites into a 2s orbital has wavelength 486.1 nm. Compute its
- frequency in Hz
- angular frequency in Hz
- wavenumber, $\bar{\nu}$, in $\text{cm}^{-1}$.
- angular wavenumber, $k$, in $\text{m}^{-1}$.
- period in s.
- momentum in m kg/s
- energy in Joules.

```python
# Give your answers as Floats. I have initialized the variables to None.
frequency = None         #Frequency in Hz.
wavenumber = None        #Wavenumber, nu-bar, in cm^-1
momentum = None          #Momentum in SI units.
energy = None            #Energy in SI units.

### BEGIN SOLUTION
# It is helpful to first convert the wavelength into m (SI units)
wavelength_m = 486.1e-9
#wavelength * frequency = speed of light. So:
frequency = constants.c/wavelength_m

#wavenumber is either 1/wavelength or 2pi/wavelength. In this context we are using nu-bar, which is the former.
#However, we want it in cm^-1. So
wavelength_cm = 485.1e-7      #Wavelength in cm
wavenumber = (wavelength_cm)**-1

#momentum = h/lambda, where h is Planck's constant. So
momentum = constants.h/wavelength_m

#Energy is h * frequency or h * c / lambda or momentum times c. Using the former,
energy = constants.h * frequency

### END SOLUTION

```

```python
print(f"The frequency of a photon with wavelength 486.1 nm is {frequency:.3e} Hz.")
print(f"The wavenumber, nu-bar, of a photon with wavelength  486.1 nm is {wavenumber:.3e} cm^-1.")
print(f"The momentum of a photon with wavelength  486.1 nm is {momentum:.3e} kg m/s.")
print(f"The energy of a photon with wavelength  486.1 nm is {energy:.3e} Joules.")

assert(isinstance(frequency,float)), "Type Error: The frequency should be a float."
assert(isinstance(wavenumber,float)), "Type Error: The wavenumber should be a float."
assert(isinstance(momentum,float)), "Type Error: The momentum should be a float."
assert(isinstance(energy,float)), "Type Error: The energy should be a float."

### BEGIN HIDDEN TESTS
assert(np.isclose(frequency,6.167e14,rtol=1e-3))
assert(np.isclose(wavenumber,2.061e4,rtol=1e-3))
assert(np.isclose(momentum,1.363e-27,rtol=1e-3))
assert(np.isclose(energy,4.086e-19,rtol=1e-3))
### END HIDDEN TESTS
```

    The frequency of a photon with wavelength 486.1 nm is 6.167e+14 Hz.
    The wavenumber, nu-bar, of a photon with wavelength  486.1 nm is 2.061e+04 cm^-1.
    The momentum of a photon with wavelength  486.1 nm is 1.363e-27 kg m/s.
    The energy of a photon with wavelength  486.1 nm is 4.086e-19 Joules.

## 🧮 How many quantum numbers are needed to label the eigenstates of the following Hamiltonian:
$$
\hat{H} =  -\frac{\hbar^2}{2m}\frac{d^2}{dx_1^2} -\frac{\hbar^2}{2m}\frac{d^2}{dy_1^2}
-\frac{\hbar^2}{2m}\frac{d^2}{dx_2^2}-\frac{\hbar^2}{2m}\frac{d^2}{dy_2^2}
+ x_1^2 + y_1^2 + x_2^2 + y_2^2
+ e^{-2\left(x_1^2 + y_1^2\right)} + e^{-2\left(x_2^2 + y_2^2\right)}
$$

```python
# Give your answer as an integer. I have initialized the variable to None
number_of_quantum_nos = None

### BEGIN SOLUTION
# There are two uncoupled particles. Each particle has two quantum numbers. This is not clear at first,
#    perhaps, since it seems like maybe the x and y coordinates are coupled in the last term. However, the
#    Hamiltonian has the form H(r1,r2,theta1,theta2), because the potential doesn't depend on the angle. So
#    it has circular symmetry and there will be an l (angular momentum) and n (principle) quantum number
#    for each of the two particles. Ergo
number_of_quantum_nos = 4

# I would also accept 6, as there are also spin quantum numbers for each particle.
### END SOLUTION
```

```python
print(f'The states of this Hamiltonian are labelled with {number_of_quantum_nos} quantum numbers.')

assert isinstance(number_of_quantum_nos,int), "Type error: The answer should be an integer."

### BEGIN HIDDEN TESTS
assert(number_of_quantum_nos == 4 or number_of_quantum_nos == 6)
### END HIDDEN TESTS

```

    The states of this Hamiltonian are labelled with 4 quantum numbers.

## 🔀Below is a list of terms in the molecular Hamiltonian, together with their mathematical expressions.
Match the verbal description of the term to its mathematical expression; the mathematical expressions are written in atomic units. Report your answer as a dictionary. The keys and values are:

**Keys:**
`electronic kinetic energy`, `nuclear kinetic energy`, `electron-electron repulsion potential`, `nucleus-nucleus repulsion potential`, `electron-nuclear attraction potential`

**Values**:
"eq1", "eq2", "eq3", "eq4", and "eq5", where

`eq1`:  $ \sum_{a=1}^{A-1} \sum_{b=a+1}^A \frac{Z_a Z_b}{\left| \mathbf{x}_a - \mathbf{x}_b \right|} $

`eq2`:  $\sum_{a=1}^A -\tfrac{1}{2m_a} \nabla^2_{\mathbf{x}_a} $

`eq3`:  $ \sum_{i=1}^N \sum_{a=1}^A -\frac{Z_a}{\left| \mathbf{r}_i - \mathbf{x}_a \right|} $

`eq4`:  $\sum_{i=1}^N -\tfrac{1}{2} \nabla^2_{\mathbf{r}_i} $

`eq5`:  $ \sum_{i=1}^{N-1} \sum_{j=i+1}^N \frac{1}{\left| \mathbf{r}_i - \mathbf{r}_j \right|} $

For example, the following is an example of an *incorrect* answer with the correct format:
```
hamiltonian_terms = {'electronic kinetic energy':'eq1', 'nuclear kinetic energy':'eq2', 'electron-electron repulsion potential':'eq3', 'nucleus-nucleus repulsion potential':'eq4', 'electron-nuclear attraction potential':'eq5'}
```

```python
# Enter your answer as a dictionary called hamiltonian_terms.
# The below dictionary has the keys typed in already for you; you need to type in the values eq1, eq2, eq3, eq4, eq5
#      instead of the place-holder values listed here.
hamiltonian_terms = {'electronic kinetic energy':'eq#',
                     'nuclear kinetic energy':'eq#',
                     'electron-electron repulsion potential':'eq#',
                     'nucleus-nucleus repulsion potential':'eq#',
                     'electron-nuclear attraction potential':'eq#'}
### BEGIN SOLUTION
hamiltonian_terms = {'electronic kinetic energy':'eq4',
                     'nuclear kinetic energy':'eq2',
                     'electron-electron repulsion potential':'eq5',
                     'nucleus-nucleus repulsion potential':'eq1',
                     'electron-nuclear attraction potential':'eq3'}
### END SOLUTION

```

```python
print(hamiltonian_terms)

assert len(hamiltonian_terms) == 5, "There should be 5 key:value pairs in the hamiltonian_terms dictionary!"

k = ['electron-electron repulsion potential', 'electron-nuclear attraction potential',
     'electronic kinetic energy', 'nuclear kinetic energy', 'nucleus-nucleus repulsion potential']
v = ['eq1', 'eq2', 'eq3', 'eq4', 'eq5']
assert sorted(hamiltonian_terms.keys()) == k, "At least one key does not match the original dictionary"
assert sorted(hamiltonian_terms.values()) == v, "At least one value does not match the equation labels"

### BEGIN HIDDEN TESTS
hamiltonian_terms_answer = {'electronic kinetic energy': 'eq4', 'nuclear kinetic energy': 'eq2',
                'electron-electron repulsion potential': 'eq5', 'nucleus-nucleus repulsion potential': 'eq1',
                'electron-nuclear attraction potential': 'eq3'}
assert(hamiltonian_terms == hamiltonian_terms_answer)
### END HIDDEN TESTS
```

    {'electronic kinetic energy': 'eq4', 'nuclear kinetic energy': 'eq2', 'electron-electron repulsion potential': 'eq5', 'nucleus-nucleus repulsion potential': 'eq1', 'electron-nuclear attraction potential': 'eq3'}

## 🔀Below are lists of different Hamiltonians we considered in the course, together with the names of functions that are related to their solutions.
Match each operator with the type of (special) functions that are its eigenfunctions. Report your answer as a dictionary. The keys and values are:

**Keys:**

`Ham1`:  $ -\frac{\hbar^2}{2m} \left(\frac{1}{r^2} \frac{d^2}{dr^2} + \frac{1}{r} \frac{d}{dr} - \frac{l^2}{r^2} \right) $

`Ham2`:  $ - \hbar^2 \left[\frac{1}{\sin \theta}\frac{d}{d\theta}\sin\theta\frac{d}{d\theta}
+ \frac{1}{\sin^2 \theta} \frac{d^2}{d\phi^2}\right] $

`Ham3`:  $ -\frac{1}{2} \left( \frac{d^2}{dr^2} + \frac{2}{r} \frac{d}{dr}\right) + \frac{l(l+1)}{2r^2} - \frac{Z}{r} $

`Ham4`:  $ \frac{d^2}{dx^2} $

**Values**:
`associated Laguerre polynomials`, `sines and cosines`, `Bessel functions`, `spherical harmonics`

For example, the following is an example of an *incorrect* answer with the correct format:
```
eigenfunctions = {'Ham1':'associated Laguerre Polynomials', 'Ham2':'sines and cosines', 'Ham3':'spherical harmonics', 'Ham4':'Bessel functions'}
```

```python
# Enter your answer as a dictionary called hamiltonian_terms.
# The below dictionary has the values typed in already for you; you need to type in the keys Ham1, Ham2, Ham3, Ham4
#      instead of the place-holder values listed here.
# eigenfunctions = {'Ham#':'associated Laguerre Polynomials',
#                   'Ham#':'sines and cosines', 0
#                   'Ham#':'spherical harmonics',
#                   'Ham#':'Bessel functions'}
### BEGIN SOLUTION
eigenfunctions = {'Ham3':'associated Laguerre Polynomials',
                  'Ham4':'sines and cosines',
                  'Ham2':'spherical harmonics',
                  'Ham1':'Bessel functions'}
### END SOLUTION
```

```python
print(eigenfunctions)

assert len(eigenfunctions) == 4, "There should be 4 key:value pairs in the eigenfunctions dictionary!"

v = ['Bessel functions', 'associated Laguerre Polynomials', 'sines and cosines', 'spherical harmonics']
k = ['Ham1', 'Ham2', 'Ham3', 'Ham4']
assert sorted(eigenfunctions.keys()) == k, "At least one Hamiltonian label does not match the original dictionary"
assert sorted(eigenfunctions.values()) == v, "At least one special function name does not match the original dictionary"

### BEGIN HIDDEN TESTS
eigenfunctions_answer = {'Ham3':'associated Laguerre Polynomials', 'Ham4':'sines and cosines',
                         'Ham2':'spherical harmonics', 'Ham1':'Bessel functions'}
assert(eigenfunctions == eigenfunctions_answer)
### END HIDDEN TESTS
```

    {'Ham3': 'associated Laguerre Polynomials', 'Ham4': 'sines and cosines', 'Ham2': 'spherical harmonics', 'Ham1': 'Bessel functions'}

## 🖩 What is the zero-point energy for an electron confined to a 2-dimensional circular disk of radius $4 \text{ a.u.}$:
- in Hartree atomic units?
- in kJ/mol (kiloJoules per mole)?

```python
# You can use the code box as a calculator if you like; either way, report your answer as float(s).
# I've initialized the answers to None.
zero_pt_E_au = None                  #Energy in Hartree (atomic units)
zero_pt_E_kJperMol = None            #Energy in kJ/mol (SI units)

### BEGIN SOLUTION
# The answer in atomic units can be evaluated directly from the notebook for particles in multi-dimensional systems
# using the widget. After you click the widget, your cursor keys can be used to make fine adjustments to its value.
zero_pt_E_au = .181

# The conversion factor to kJ/mol is 2625.5 kJ/mol per Hartree. So:
zero_pt_E_kJperMol = .181 * 2625.5
### END SOLUTION
```

```python
print(f'The zero-point energy of a electron confined to a disk of radius 4 Bohr is {zero_pt_E_au:.3f} Hartree.')
print(f'The zero-point energy of a electron confined to a disk of radius 4 Bohr is {zero_pt_E_kJperMol:.3f} kJ/mol.')

# basic tests:
assert(isinstance(zero_pt_E_au,float)), "The answer should be an float"
assert(isinstance(zero_pt_E_kJperMol,float)), "The answer should be an integer"

### BEGIN HIDDEN TESTS
assert(np.isclose(zero_pt_E_au,0.181,rtol=1e-2))
assert(np.isclose(zero_pt_E_kJperMol,474.215,rtol=1e-2))
### END HIDDEN TESTS
```

    The zero-point energy of a electron confined to a disk of radius 4 Bohr is 0.181 Hartree.
    The zero-point energy of a electron confined to a disk of radius 4 Bohr is 475.215 kJ/mol.

## ✍ Write a Slater determinant for the excited-state, $1\text{s}^2 2\text{s}^1 3\text{s}^1$ configuration of the Beryllium atom.
Assume that the unpaired electrons have the same spin. You may denote the spatial orbitals as $\phi_{1s}(\mathbf{r})$, $\phi_{2s}(\mathbf{r})$ and $\phi_{3s}(\mathbf{r})$.

=== BEGIN MARK SCHEME ===

$$
\Psi(\mathbf{r}_1,\mathbf{r}_2,\mathbf{r}_3,\mathbf{r}_4) = \frac{1}{\sqrt{4!}}
\begin{vmatrix}
\phi_{1s}(\mathbf{r}_1)|\alpha(1)\rangle &\phi_{1s}(\mathbf{r}_1)|\beta(1)\rangle & \phi_{2s}(\mathbf{r}_1)|\alpha(1)\rangle & \phi_{3s}(\mathbf{r}_1)|\alpha(1)\rangle \\
\phi_{1s}(\mathbf{r}_2)|\alpha(2)\rangle &\phi_{1s}(\mathbf{r}_2)|\beta(2)\rangle & \phi_{2s}(\mathbf{r}_2)|\alpha(2)\rangle & \phi_{3s}(\mathbf{r}_2)|\alpha(2)\rangle \\
\phi_{1s}(\mathbf{r}_3)|\alpha(3)\rangle &\phi_{1s}(\mathbf{r}_3)|\beta(3)\rangle & \phi_{2s}(\mathbf{r}_3)|\alpha(3)\rangle & \phi_{3s}(\mathbf{r}_3)|\alpha(3)\rangle \\
\phi_{1s}(\mathbf{r}_4)|\alpha(4)\rangle &\phi_{1s}(\mathbf{r}_4)|\beta(4)\rangle & \phi_{2s}(\mathbf{r}_4)|\alpha(4)\rangle & \phi_{3s}(\mathbf{r}_4)|\alpha(4)\rangle
\end{vmatrix}
$$

You could also choose the 2s and 3s electrons to have $\beta$ spin.

=== END MARK SCHEME ===

## 🧮 Compute properties of a ${}^{7}\text{F}$ state.
The ground-state term symbol for Samarium (Sm, atomic number 62) is ${}^{7}\text{F}_0$.
- How many nominally spatial and spin states are associated with the ${}^{7}\text{F}$ term symbol? Answer with an integer.
- In addition to J=0, what other J values are possible for the ${}^7{\text{F}}$ term symbol? Answer with a set or list of integers.

For example, for a triplet P state, we would have
```
multiplicity_3P = 9
Jvalues_3P = [0,1,2]
```

```python
# Multiplicity of septuplet F state of Samarium (integer)
multiplity_7F = None

# J values associated with septuplet F state of Samarium (set or list of integers)
Jvalues_7F = None

### BEGIN SOLUTION
# There are 7 spin states. An F-term corresponds to L=3, with 7 orbital antular momentum states (M_L=-3,-2,-1,0,1,2,3).
multiplicity_7F = 7*7

# Since S = 3 and L = 3, the J values are |L-S|=0, |L-S|+1=1, ..., L+S = 6
Jvalues_7F = [0,1,2,3,4,5,6]
### END SOLUTION

```

```python
print(f'A septuplet-F term is {multiplicity_7F}-fold degenerate and has J values {Jvalues_7F}.')

assert(isinstance(multiplicity_7F,int)), "The multiplicity should be an integer"
assert(isinstance(Jvalues_7F,set) or isinstance(Jvalues_7F,list) or isinstance(Jvalues_7F,tuple)), "The list of J values should be a list of integers."
assert(len(Jvalues_7F) >= 1)

### BEGIN HIDDEN TESTS
assert(multiplicity_7F == 49)
assert(set(Jvalues_7F) == {0,1,2,3,4,5,6})
### END HIDDEN TESTS
```

    A septuplet-F term is 49-fold degenerate and has J values [0, 1, 2, 3, 4, 5, 6].

## ✍ Solve the Schrödinger equation for a muon, $\mu^-$, bound to Neon nucleus.
In [muonic atoms](https://en.wikipedia.org/wiki/Exotic_atom), a muon replaces an electron. The muon and the electron are both fermions, and they have the same charge. However, the mass of the muon is $207$ times the mass of the electron, $m_\mu = 206.8 m_e$.

Assuming the Born-Oppenheimer approximation is valid and that relativistic effects can be neglected, **what are the energy eigenfunctions and eigenvalues for a system composed of one $\mu^-$ bound to a Neon nucleus.**

=== BEGIN MARK SCHEME ===

For convenience, I will work in atomic units. The time-independent Schrödinger equation, assuming the Born-Oppenheimer approximation, is then:
$$
\left(-\frac{1}{2 m_\mu} \nabla^2 - \frac{Z}{r} \right) \Psi(\mathbf{r}) = E\Psi(\mathbf{r})
$$
This is very close to the hydrogenic atom. Multiply both sides of the equation by $m_\mu$ to obtain a hydrogenic system,

$$
\left(-\frac{1}{2} \nabla^2 -\frac{Z m_\mu}{r} \right) \Psi(\mathbf{r}) = Em_\mu \Psi(\mathbf{r})
$$
This is just the Hydrogenic atom, but with the atomic charge replaced by $Z m_\mu$.

**Eigenvalues:**
This means that
$$
E m_\mu = -\frac{(Z m_\mu)^2}{2n^2}
$$
and so:
$$
E_n = -m_\mu \frac{Z^2}{2n^2} = - \frac{20700}{2n^2} = -\frac{10350}{n^2}
$$
In the last line, the atomic number of Neon ($Z=10$) and the mass of the muon ($m_\mu = 207$) were used.

**Eigenvectors:**
The eigenvectors are similar; one notices that they should be the same as the normal Hydrogenic eigenvectors, but with $Z$ replaced by $Z m_\mu$. So:
$$
\psi_{nlm}(r,\theta,\phi) \propto \left(\frac{2Z m_\mu r}{n}\right)^l L_{n-1-l}^{2l+1}\left(\frac{2Z m_\mu r}{n}\right) e^{-{Z m_\mu r}/{n}}  Y_l^{m} (\theta, \phi)
$$

=== END MARK SCHEME ===

## ✍ Write an expression for the lowest-energy molecular orbital of $\text{H}_3^{+2}$.
You may assume that the system is well-described by the separated-atom limit, and that the Hydrogen atoms are arranged as an equilateral triangle ($\text{D}_{3h}$ symmetry). You do not need to normalize the molecular orbital.

=== BEGIN MARK SCHEME ===

Because we are near the separated atom limit, the linear combination of atomic orbitals (LCAO) approach
should be acceptable. So, labeling the Hydrogen atoms as $a$, $b$, and $c$, we have
$$
\psi_{\text{H}_3^{+2}}(\mathbf{r}) = c_a \phi_{\text{1s}}^{(a)}(\mathbf{r})
+ c_b \phi_{\text{1s}}^{(b)}(\mathbf{r})
+ c_c \phi_{\text{1s}}^{(c)}(\mathbf{r})
$$
Because we are in the ground state, there are no nodes. That means that $c_a$, $c_b$, and $c_c$ all have the same sign (they are all positive or all negative. Because we are in a $\text{D}_3h$ geometry, all the hydrogen atoms are equivalent, which means that $c_a = c_b = c_c$. So
$$
\psi_{\text{H}_3^{+2}}(\mathbf{r}) = c \phi_{\text{1s}}^{(a)}(\mathbf{r})
+ c \phi_{\text{1s}}^{(b)}(\mathbf{r})
+ c \phi_{\text{1s}}^{(c)}(\mathbf{r})
$$

=== END MARK SCHEME ===

## The next two questions refer to the Laplacian operator.
In this question you will show, mathematically, that two properties of the Laplacian operator are true. It is OK (but not essential) to assume that you are interested in the Laplacian operator in 1 dimension, $\nabla^2 = \frac{d^2}{dx^2}$.
Note: You may assume that the wavefunction, and its first first and second derivatives, vanish at the ends of the region of integration. However, there are also ways to answer these questions without explicitly invoking those assumptions.

### ✍ Show that $\nabla^2$ is a linear, Hermitian operator.

=== BEGIN MARK SCHEME ===

Both parts of this question were taken from an old exam.

To show that $\nabla^2$ is linear, we need only recall that it is the second derivative operator. We know that differentiation is a linear operator. I.e., differentiation of a constant times a function is equal to a constant, times the value obtained by differentiating the function. Similarly, differentiation of a sum of functions is equal to summing the derivatives of the functions. Mathematically,
$$
\frac{d^k}{dx^k} \text{constant} \cdot f(x) = \text{constant} \cdot\frac{d^kf}{dx^k}
$$
and
$$
\frac{d^k\left(f(x) + g(x) \right)}{dx^k} = \frac{d^kf(x)}{dx^k} + \frac{d^kg(x)}{dx^k}
$$
So $\nabla^2$ is linear.

One can show that $\nabla^2$ is Hermitian using integration by parts, similar to how we did in the course notes. You can also recognize that the Hermitian property follows directly from [Green's second identity](https://en.wikipedia.org/wiki/Green%27s_identities#Green's_second_identity) . (In both cases you use the fact that the wavefunction and its derivatives vanish at the end of the interval of integration.)

However, we also know that $\nabla^2$ is closely related to the momentum operator.
$$
\hat{p}^2 = -i \hbar \nabla \cdot -i \hbar \nabla = - \hbar^2 \nabla^2
$$
In atomic units, then, $\nabla^2 = - \hat{p}^2$. The following math uses the fact that the momentum operator is Hermitian.

We need to show that
$$
\int \Phi(x)^* \nabla^2 \Psi(x) dx = \int \left( \nabla^2 \Phi(x) \right)^* \Psi(x) dx
$$
To this end, we start with the relationship between the Laplacian and the momentum operator, then (repeatedly) invoke the fact the momentum operator is Hermitian. So:
\begin{align}
\int \Phi(x)^* \nabla^2 \Psi(x) dx &=\int \Phi(x)^* -\hat{p}^2 \Psi(x) dx \\
&=-\int \Phi(x)^* \hat{p}\hat{p} \Psi(x) dx \\
&=-\int \left(\hat{p}\Phi(x)\right)^* \hat{p} \Psi(x) dx \\
&=-\int \left(\hat{p}\hat{p}\Phi(x)\right)^* \Psi(x) dx \\
&=\int \left(-\hat{p}^2\Phi(x)\right)^* \Psi(x) dx \\
&=\int \left(\nabla^2\Phi(x)\right)^* \Psi(x) dx
\end{align}
=== END MARK SCHEME ===

### ✍ Show that $\nabla^2$ is negative definite.
That is, show that for *any* $\Psi(\mathbf{r})$, it is *always* true that
$$
\langle \Psi | \nabla^2 | \Psi \rangle < 0
$$

=== BEGIN MARK SCHEME ===

One can show that $\nabla^2$ is negative-definite using [Green's first identity](https://en.wikipedia.org/wiki/Green%27s_identities#Green's_first_identity) or integration by parts. However, as with the first part of this problem, one can also directly invoke the connection to the momentum operator, $\nabla^2 = - \hat{p}^2$. Since the square of an operator is positive definite, it's clear that the negative of the operator is negative definite. However, more explicitly, one can write:
\begin{align}
\int \Psi(x)^* \nabla^2 \Psi(x) dx &=\int \Psi(x)^* -\hat{p}^2 \Psi(x) dx \\
&=-\int \Psi(x)^* \hat{p}\hat{p} \Psi(x) dx \\
&=-\int \left(\hat{p}\Psi(x)\right)^* \hat{p} \Psi(x) dx \\
&=-\int \left|\hat{p}\Psi(x)\right|^2 dx \\
&<0
\end{align}

=== END MARK SCHEME ===

## Expectation values of $r^k$ and related properties in Hydrogenic atoms.
The goal of this problem is to explore the radial distribution function and its expectation values for hydrogenic atoms. A key integral that will be useful is
$$
\int_0^{\infty} r^n e^{-\beta r} dr = \frac{n!}{\beta^{n+1}}
$$

It is also useful to know [Kramer's recursion relation](https://phlconnect.ched.gov.ph/admin/uploads/81dc9bdb52d04dc20036dbd8313ed055/53-hydrogen-4.pdf) for radial expectation values. If $\psi_{nlm}(r,\theta,\phi)$ denotes a normalized hydrogenic wavefunction, then Kramer's recursion states that:
$$
\langle \psi_{nlm} | r^k | \psi_{nlm} \rangle = a_{n}(k,Z) \langle \psi_{nlm} | r^{k-1} | \psi_{nlm} \rangle + b_{nl}(k,Z) \langle \psi_{nlm} | r^{k-2} | \psi_{nlm} \rangle
$$
where

\begin{align}
a_{n}(k,Z) &=  \frac{n^2(2k+1)}{(k+1)Z}\\
b_{nl}(k,Z) &= \frac{n^2 k \left(k^2 - (2l+1)^2\right)}{4Z^2(k+1)}
\end{align}
A detailed derivation of this relation can be found [here](http://www.blazartheory.com/files/notes/qmnotes/Kramers__Relation.pdf). You will not *need* to use this relation in this problem; it's a matter of preference whether you use this relation or perform integrals.

Throughout this problem you may use atomic units.

### ✍ Expectation value of $r^{-1}$ for Hydrogenic Atoms.
What is the expectation value of $r^{-1}$ for a Hydrogenic atom. That is, what is the expectation value of
$$
\langle \psi_{nlm} | r^{-1} | \psi_{nlm} \rangle
$$
for an electron bound to a nuclear of charge Z. *Report your answer as a float, in atomic units.*

=== BEGIN MARK SCHEME ===

The problems in this question are (at most) small revisions to problems from old exams posted on the web site.

This first question is most easily solved using the Hellmann-Feynman theorem. Note that for a Hydrogenic atom,
$$
\frac{d\hat{H}}{dZ} = -\frac{1}{r}
$$
Furthermore,
$$
\frac{dE}{dZ} = \frac{d}{dZ} \frac{-Z^2}{2n^2} = -\frac{Z}{n^2}
$$
So,
$$
\int \psi_{nlm}(\mathbf{r})^* \left[ \frac{1}{r}\right] \psi_{nlm}(\mathbf{r}) d \mathbf{r} = \int \psi_{nlm}(\mathbf{r})^* \left[-\frac{dH}{dZ} \right]\psi_{nlm}(\mathbf{r}) d \mathbf{r} = - \frac{dE}{dZ} = \frac{Z}{n^2}
$$
In the next-to-last equality we used the Hellmann-Feynman theorem.

=== END MARK SCHEME ===

### 🖩 Expectation value of $r^2$ for a 4f orbital in the hydrogen atom.
What is the expectation value of $r^2$ for the 4f orbital in the hydrogen atom (Z=1). That is, what is the expectation value of
$$
\langle \psi_{43m} | r^2 | \psi_{43m} \rangle
$$
in the Hydrogen atom? *Report your answer as a float, in atomic units.*

If you wish, you can use the markdown cell at the end to explain your answer.

```python
# Expectation value of r^2 for a 4f orbital in the hydrogen atom (Z=1) in atomic units (Bohr^2).
# I have initialized the variable to None. Give your answer in atomic units (Bohr).
avg_r2 = None

### BEGIN SOLUTION
avg_r2 = 360   #See below for explanation.
### END SOLUTION
```

```python
print(f'The expectation value of r^2 for an electron in the 4f orbital of the hydrogen atom is {avg_r2:.2f} Bohr^2.')

### BEGIN HIDDEN TESTS
assert(np.isclose(avg_r2,360.,rtol=1e-2))
### END HIDDEN TESTS
```

    The expectation value of r^2 for an electron in the 4f orbital of the hydrogen atom is 360.00 Bohr^2.

You can use this markdown cell to (optionally) describe how you obtained your answer.

=== BEGIN MARK SCHEME ===
This problem could be done numerically, or using the Kramer's recursion, or by explicit integration. The key step to doing it with Kramer's relation is to note that because the hydrogenic wavefunctions are normalized,
$$
\langle \psi_{nlm} |r^0| \psi_{nlm} \rangle = \langle \psi_{nlm} | \psi_{nlm} \rangle = 1
$$
This means that you have the results for $k=-1$ (from the previous part of this problem) and $k=0$ (from normalization). So you can deduce expressions for k=1
\begin{align}
\langle \psi_{nlm} |r| \psi_{nlm} \rangle &= \frac{3\cdot4^2}{2} \langle r^0 \rangle + \frac{4^2(1-7^2)}{8} \langle r^{-1} \rangle \\
&= 24 - 6 = 18 \text{ Bohr}
\end{align}
\begin{align}
\langle \psi_{nlm} |r^2| \psi_{nlm} \rangle &= \frac{5\cdot4^2}{3} \langle r^1 \rangle + \frac{32(4-7^2)}{12} \langle r^{0} \rangle \\
&= 360 \text{ Bohr}^2
\end{align}

I will show how to do it with explicit integration. The key is to remember that the spherical harmonics are orthonormal,
$$
\int_0^\pi \int_0^{2 \pi} Y_l^m(\theta,\phi)^* Y_{l'}^{m'}(\theta,\phi) \sin \theta d\phi d\theta = \delta_{l l'} \delta_{m m'}
$$
We also need to remember that for $l=n-1$, the radial wavefunction is proportional to $r^l e^{-Zr/n}$. So for these cases, we have:
\begin{align}
\langle \psi_{n,n-1,m} | r^k | \psi_{n,n-1,m} \rangle
&= \frac{\int_0^{\infty} \int_0^\pi \int_0^{2 \pi} r^{n-1} e^{-Zr/n} Y_{n-1}^m(\theta,\phi)^*\left[r^k \right] r^{n-1} e^{-Zr/n}Y_{n-1}^{m}(\theta,\phi) r^2\sin \theta d\phi d\theta dr}{\int_0^{\infty} \int_0^\pi \int_0^{2 \pi} r^{n-1} e^{-Zr/n} Y_{n-1}^m(\theta,\phi)^* r^{n-1} e^{-Zr/n}Y_{n-1}^{m}(\theta,\phi) r^2\sin \theta d\phi d\theta dr} \\
&= \frac{\int_0^{\infty} r^{2n+k}e^{-2Zr/n} dr}{\int_0^{\infty} r^{2n}e^{-2Zr/n}dr} \\
&= \frac{(2n+k)!\left(\tfrac{2Z}{n}\right)^{2n+1}}{(2n)!\left(\tfrac{2Z}{n}\right)^{2n+k+1}} \\
&= \frac{n^k(2n+k)(2n+k-1)\cdots(2n+1)}{2^kZ^k}
\end{align}
Therefore we have that
$$
\langle \psi_{4,3,m} | r | \psi_{4,3,m} \rangle = 18 \text{ Bohr}
$$
$$
\langle \psi_{4,3,m} | r^2 | \psi_{4,3,m} \rangle = 360 \text{ Bohr}^2
$$

=== END MARK SCHEME ===

### 🖩 What is the most probable distance to find an electron from the nucleus in the 4f orbital of a hydrogen atom?
Report your answer in Bohr (atomic units).

If you wish, you can use the markdown cell at the end to explain your answer.

```python
# Most probable distance at which to observe an electron in the 4f orbital from the nucleus of the hydrogen atom.
# I have initialized the variable to None. Give your answer in atomic units (Bohr).
dist_mostprob = None

### BEGIN SOLUTION
dist_mostprob = 16   #See below for explanation.
### END SOLUTION
```

```python
print(f'The most probable value of the distance from the nucleus for a 4f electron in Hydrogen is {dist_mostprob:.2f} Bohr.')

### BEGIN HIDDEN TESTS
assert(np.isclose(dist_mostprob,16,rtol=1e-2))
### END HIDDEN TESTS
```

    The most probable value of the distance from the nucleus for a 4f electron in Hydrogen is 16.00 Bohr.

You can use this markdown cell to (optionally) describe how you obtained your answer.

=== BEGIN MARK SCHEME ===
This problem could be done numerically, but I'll show the analytic solution. The key is to understand that the eigenfunction of the 4f orbital, and any orbital that has $l=n-1$, is simply
$$
psi_{n,n-1,m}(r, \theta, \phi) \propto r^{n-1} e^{-Zr/n} Y_{n-1}^{m}(\theta,\phi)
$$
and the probability of observing a particle at r units from the nucleus at *some* value of $(\theta,\phi)$ requires integration over $d\theta d\phi$. So, using $p(r_0)$ to denote the probability of observing a particle $r_0$ units from the nucleus,
$$
p(r_0) \propto \int_0^{\infty} \int_0^\pi \int_0^{2 \pi} r^{n-1} e^{-Zr/n} Y_{n-1}^m(\theta,\phi)^*\left[\delta(r-r_0) \right] r^{n-1} e^{-Zr/n}Y_{n-1}^{m}(\theta,\phi) r^2\sin \theta d\phi d\theta dr
$$
or simply
$$
p(r) \propto r^{2n} e^{-2Zr/n}
$$
We could have inferred this without all the mathematics by merely noting that the surface area of a sphere is proportional to $r^2$, so the probability of observing an electron on a spherical shell of radius $r$ is r^2 times the radial wavefuncton squared.

To find the most-probable distance, we need to differentiate this equation and set the derivative equal to zero. So
$$
0 = \frac{dp}{dr} = (2n)r^{2n-1}e^{-2Zr/n} + r^{2n} \left( \tfrac{-2Z}{n} \right) e^{-2Zr/n} = \left(2n - \tfrac{-2Z}{n}r \right)r^{2n-1} e^{-2Zr/n}
$$
So,
$$
r_{\text{most probable}} = \frac{n^2}{Z}
$$
For the 4f (n=4) orbital of Hydrogen (Z=1), this is just 16.

=== END MARK SCHEME ===

### 🖩 Heisenberg Uncertainty Principle
The Heisenberg Uncertainty Principle between momentum and position in 3 dimensions states that
$$
\sigma_r^2 \sigma_p^2 \ge  \tfrac{3}{4} \hbar^2
$$
where
$$
\sigma_r^2 = \langle \psi_{n,n-1,m} | r^2 | \psi_{n,n-1,m} \rangle - \langle \psi_{n,n-1,m} | r | \psi_{n,n-1,m} \rangle^2 \\
\sigma_p^2 = \langle \psi_{n,n-1,m} | \hat{p}^2 | \psi_{n,n-1,m} \rangle - \langle \psi_{n,n-1,m} | \hat{p} | \psi_{n,n-1,m} \rangle^2
$$
To reassure yourself that this is true, compute $\sigma_r^2 \sigma_p^2$ for the 4f state of the hydrogen atom. *Report your answer as a float, in atomic units.*

If you wish, you can use the markdown cell at the end to explain your answer.

```python
# Write the left-hand-side of the Heisenberg Uncertainty relation, which should be bigger than 3/2 hbar.
# Give your answer in atomic units.
test_Heisenberg = None

### BEGIN SOLUTION
test_Heisenberg = 2.25   #See below for explanation.
### END SOLUTION
```

```python
print(f'The product of the variance in position and momentum for an electron in the 4f orbital of Hydrogen is {test_Heisenberg:.2f} times hbar.')
assert(test_Heisenberg >= 0.75), "The Heisenberg Uncertainty Principle has been violated!"

### BEGIN HIDDEN TESTS
assert(np.isclose(test_Heisenberg,2.25,rtol=1e-2))
### END HIDDEN TESTS
```

    The product of the variance in position and momentum for an electron in the 4f orbital of Hydrogen is 2.25 times hbar.

You can use this markdown cell to (optionally) explain how you obtained your answer.

=== BEGIN MARK SCHEME

Like the previous questions in this problem, this question was taken off a recent exam. We have already computed the first part of what we need, which is (see the question before last)
$$
\sigma_r^2 = 360 - 18^2 = 36 \text{ Bohr}^2
$$
We also know that because the hydrogen atom is in its 4f stationary state, there is no *net* motion of the electrons, so $\langle \hat{p} \rangle = 0$. We realize that the kinetic energy of the Hydrogen atom, in atomic units, is $\langle \hat{p}^2/2 \rangle$, so
$$
\sigma_p^2 = \langle \hat{p}^2 \rangle = 2 [\text{kinetic energy}]
$$
The kinetic energy can be inferred in several different ways. For example, we could deduce it from the Hellmann-Feynman theorem, by differentiated the energy with respect to $\hbar$. However, in the first part of this question, we learned that
$\langle r^{-1} \rangle = \tfrac{Z}{n^2}$, which implies that the potential energy for the Hydrogen atom is $\langle -Zr^{-1} \rangle = -\tfrac{Z^2}{n^2}$. Now
$$
E = \text{kinetic energy + potential energy}
$$
and we know now that
$$
-\frac{Z^2}{2n^2} = \langle \hat{T} \rangle + -\frac{Z^2}{n^2}
$$
So
$$
\langle \hat{T} \rangle = \frac{Z^2}{2n^2}
$$
and
$$
\sigma_p^2 = \langle \hat{p}^2 \rangle = 2\langle \hat{T} \rangle = \frac{Z^2}{n^2}
$$
So, for an electron in the 4f (n=4) orbital of Hydrogen (Z=1),
$$
\sigma_r^2 \sigma_p^2 = 36 \cdot \tfrac{1}{16} = \tfrac{9}{4} = 2.25 > \frac{3}{4}
$$

=== END MARK SCHEME

### 💰 Bonus: Derive an expression for $\sigma_r^2 \sigma_p^2$ for *any* state of a Hydrogenic atom with $l=n-1$.

=== BEGIN MARK SCHEME ===

Almost everything you need to do for this problem is already done. From three problems ago,
$$
\langle \psi_{n,n-1,m} | r^k | \psi_{n,n-1,m} \rangle = \frac{n^k(2n+k)(2n+k-1)\cdots(2n+1)}{2^kZ^k}
$$
and specifically,
$$
\langle \psi_{n,n-1,m} | r^2 | \psi_{n,n-1,m} \rangle = \frac{n^2(2n+2)(2n+1)}{4Z^2}
\langle \psi_{n,n-1,m} | r | \psi_{n,n-1,m} \rangle = \frac{n(2n+1)}{2Z}
$$
So
$$
\sigma_r^2 = \frac{n^2(2n+2)(2n+1)}{4Z^2} -  \frac{n^2(2n+1)^2}{4Z^2} = \frac{n^2(2n+1)}{4Z^2}
$$
From the previous problem
$$
\sigma_p^2 = \frac{Z^2}{n^2}
$$
So
$$
\sigma_r^2 \sigma_p^2 = \frac{2n+1}{4} \ge \frac{3}{4}
$$
In the ground state ($n=1$), one is just at the threshhold of violating the uncertainty principle; as $n$ increases the mutual uncertainty in momentum and position increases.

=== END MARK SCHEME ===

## Eigenfunctions and Eigenvalues for a two-dimensional Quantum Dot.
Modern displays often use quantum dot technology, where one (or more) electrons are confined to a region within a material. For an electron confined to a rectangular region in two dimensions, it is reasonable to approximate its motion perpendicular to the rectangle with harmonic confinement. The time-independent Schrödinger equation, eigenfunctions, and eigenvalues for a harmonically confined electron are, in atomic units:
$$
\left(-\tfrac{1}{2}\tfrac{d^2}{dz^2} + \tfrac{1}{2}\kappa z^2\right) \psi_k(z) = E_k \psi_k(z)
$$
where the eigenenergies are
$$
E_k = \sqrt{\kappa}\left(k+\tfrac{1}{2}\right) \qquad \qquad k=0,1,2,\ldots
$$
and the eigenfunctions are given in terms of the [Hermite polynomials](https://en.wikipedia.org/wiki/Hermite_polynomials), $H_k(z)$, as:
$$
\psi_k^{(\text{harm. osc.})}(z) =\frac{1}{2^k k!}\sqrt[4]{\frac{\kappa}{\pi}}
e^{-\sqrt{\kappa}z^2/2}H_k\left(\sqrt[4]{\kappa}z\right)
$$
The Hamiltonian for an electron confined to a rectangular region, in atomic units, is then:
$$
\hat{H} = -\frac{1}{2} \frac{d^2}{dx^2} -\frac{1}{2} \frac{d^2}{dy^2} -\frac{1}{2} \frac{d^2}{dz^2}
 + V_{a_x}(x) + V_{a_y}(y) + \frac{1}{2} \kappa z^2
$$
where
$$
V_a(x) =
\begin{cases}
    +\infty & x\leq 0\\
    0       & 0\lt x \lt a\\
    +\infty & a \leq x
\end{cases}
$$

### ✍ Write expressions for the eigenvalues and eigenfunctions of this system.

=== BEGIN MARK SCHEME

This system is a sum of three Hamiltonians, two of which are 1-dimensional particles-in-a-box and one of which is a 1-dimensional Harmonic Oscillator.
$$
\hat{H} = \hat{H}_x + \hat{H}_y + \hat{H}_z
$$
with
$$
\hat{H}_x = -\frac{1}{2} \frac{d^2}{dx^2} + V_{a_x}(x) \\
\hat{H}_y = -\frac{1}{2} \frac{d^2}{dy^2} + V_{a_y}(y) \\
\hat{H}_z = -\frac{1}{2} \frac{d^2}{dz^2} + \frac{1}{2} \kappa z^2
$$
By separation of variables, the eigenenergies are the sums of the eigenenergies of the components' energies which, using the formula for the energies of the particle-in-a-box in atomic units, gives
$$
E_{n_x,n_y,k} = \frac{n_x^2 \pi^2}{2a_x^2} + \frac{n_y^2 \pi^2}{2a_y^2} + \sqrt{\kappa}\left(k+\tfrac{1}{2}\right) \qquad n_x,n_y = 1,2,\ldots \quad k=0,1,\ldots
$$
and the eigenfunctions are the products of the individual components' eigenfunctions
$$
\psi_{n_x,n_y,k}(x,y,z) = \frac{2}{\sqrt{a_x a_y}} \sin\left(\frac{n_x \pi x}{a_x}\right) \sin\left(\frac{n_y \pi y} {a_y}\right) \cdot\frac{1}{2^k k!}\sqrt[4]{\frac{\kappa}{\pi}}
e^{-\sqrt{\kappa}z^2/2}H_k\left(\sqrt[4]{\kappa}z\right)
$$

=== END MARK SCHEME

###  🖩 What is the wavelength that corresponds to the lowest-energy excitation when $a_x = 16$, $a_y = 9$, and $k_z = 4$?
Report your answer in nm.

If you wish, you can use the markdown cell at the end to explain your answer.

```python
# Report the wavelength of the lowest-energy excitation in nm. I have initialized the variable to None.
wavelength_qdot_nm = None

### BEGIN SOLUTION
# The energy of excitation is
excitation_energy = (4 - 1) * np.pi**2 / (2 * 16**2)
# The wavelength in atomic units is thus
wavelength_qdot_au = 2 * np.pi * 137.036 / excitation_energy
# The wavelength in nanometers is, finally
wavelength_qdot_nm = wavelength_qdot_au * 0.0529177
### END SOLUTION
```

```python
print(f'The wavelength associated with the lowest-energy excitation of the specified quantum dot is {wavelength_qdot_nm:.1f} nm.')

### BEGIN HIDDEN TESTS
assert(np.isclose(wavelength_qdot_nm,787.9,rtol=1e-3))
### END HIDDEN TESTS
```

    The wavelength associated with the lowest-energy excitation of the specified quantum dot is 787.9 nm.

You can use this markdown cell to (optionally) explain your answer

=== BEGIN MARK SCHEME

The ground state energy is easily determined for this system. The question is what the lowest excited state will be. It will correspond to an excitation in the $x$, $y$, or $z$ direction. The box in the $x$ direction is larger, which means its energy levels are more closely spaced. So the excitation will be in either the $x$ or $z$ direction. We should check. The excitations corresponding to the possibilities are
$$
E_x^{(1\rightarrow2)} = \frac{(4-1)\pi^2}{512} = .0578 \\
E_y^{(1\rightarrow2)} = \frac{(4-1)\pi^2}{162} = .1828 \\
E_z^{0\rightarrow1} = (1-0)\sqrt{4} = 2
$$
So the lowest-energy excitation occurs in the $x$ direction, which is intuitive given the nature of the system being described.

The excitation energy of 0.0578 can be converged into wavelength in several different ways. The easiest is to recall that the speed of light in atomic units is 137.036. So from
$$
E = \frac{hc}{\lambda} = \frac{2 \pi \hbar c}{\lambda}
$$
we have, in atomic units, that
$$
\lambda = \frac{2 \pi 137.036}{E} = \frac{2\cdot512 \pi 137.036}{3 \pi^2} = \frac{1024 \cdot 137.036}{3 \pi} = 14889 a.u.
$$
To convert this from Bohr to nanometers, we use the fact that 1 Bohr = 0.0529177 nm. So
$$
\lambda = 787.9 nm
$$

=== END MARK SCHEME

## ✍ What is the form of the eigenfunctions for the Kratzer-Fues Potential
The Kratzer-Fues potential is a reasonable model for a diatomic molecule rotating and vibrating in 3 dimensions, or even a ion-pair complex (e.g., an single ion pair from an ionic solvent in the gas phase). It has the form
$$
V_{\text{Kratzer-Fues}}(r) = \frac{a}{r^2} - \frac{b}{r} \qquad \qquad a>0 \text{ and } b>0
$$
and so the Schrödinger equation has the form:
$$
\left(-\frac{\hbar^2}{2m}\nabla^2 +\frac{a}{r^2} - \frac{b}{r} \right) \psi(\mathbf{r}) = E\psi(\mathbf{r})
$$
Using separation of variables, write the *form* of the solutions for the Kratzer-Fues potential. What is the radial Schrödinger equation for this system?

=== BEGIN MARK SCHEME ===

This is a spherically-symmetric Schrödinger equation in 3 dimensions. This means that the solution is  the product of a radial part and a spherical harmonic,
$$
\psi_{nlm_l}(r,\theta,\phi) = R_{nl}(r) Y_l^{m_l} (\theta,\phi)
$$
and the radial wavefunction, $R_{nl}(r)$ satisfies the radial Schrödinger equation for a 3-dimensional system,
$$
\left(-\frac{\hbar^2}{2m} \left( \frac{d^2}{dr^2}
+ \frac{2}{r} \frac{d}{dr}\right)
      + \frac{\hbar^2 l(l+1)}{2mr^2} + \frac{a}{r^2} - \frac{b}{r} \right)
      R_{n,l}(r)
= E_{n,l}R_{n,l}(r)
$$
or, simplifying slightly,
$$
\left(-\frac{\hbar^2}{2m} \left( \frac{d^2}{dr^2}
+ \frac{2}{r} \frac{d}{dr}\right)
      + \frac{\hbar^2 l(l+1) + 2ma}{2mr^2} - \frac{b}{r} \right)
      R_{n,l}(r)
= E_{n,l}R_{n,l}(r)
$$

=== END MARK SCHEME ===

### 💰 Bonus: Write an expression for the energy eigenvalues and the radial eigenfunctions of the Kratzer-Fues potential. This isn't 100% trivial, but try to see how far you can get.

=== BEGIN MARK SCHEME

Notice that the radial Hamiltonian for the Kratzer-Fues potential looks *a lot* like the Hydrogenic atom, which has the Hamiltonian,
$$
\left(-\frac{1}{2} \left( \frac{d^2}{dr^2}
+ \frac{2}{r} \frac{d}{dr}\right)
      + \frac{l(l+1)}{2r^2} - \frac{Z}{r} \right)
      R_{n,l}(r)
= E_{n,l}R_{n,l}(r)
$$
Returning to the Kratzer-Fues case, if one uses atomic units multiplies both sides of the equation by $m$, then
$$
\left(-\frac{1}{2} \left( \frac{d^2}{dr^2}
+ \frac{2}{r} \frac{d}{dr}\right)
      + \frac{l(l+1) + 2ma}{2r^2} - \frac{bm}{r} \right)
      R_{n,l}(r)
= \left(mE_{n,l}\right)R_{n,l}(r)
$$
If you then defined a value, $l'$, such that
$$
l'(l'+1) = l(l+1) + 2ma
$$
the solutions would be Hydrogenic,
$$
E_{\text{Kratzer-Fues},n} = -\frac{mb^2}{2n^2}
$$
and
$$
R_{n,l}(r) \propto \left( \frac{2mbr}{n} \right)^{l'} L_{n-1-l'}^{2l'+1}\left(\frac{2mbr}{n}\right) e^{-\frac{mbr}{n}}
$$
This treatment is a bit naive, insofar as there is no guarantee that l' is an integer, so defining the associated Laguerre polynomial of a non-integer degree may be required. Such a polynomial can be defined by generalizing the associated Laguerre functions to noninteger degree using hypergeometric functions. However, that's further than I wished to go.

In practice, this solution is not quite right except for the exceptional cases where $l'$ is an integer less than $n$. But this is the *idea* of the solution, which is all that is really important for this bonus. The exact solution can be found various places, see, [for example](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.741.7716&rep=rep1&type=pdf). The detailed solution can be found at in
> S. Flügge *Practical Quantum Mechanics* (Berlin:Springer, 1957)

=== END MARK SCHEME


##### Copyright 2020 The TensorFlow Quantum 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.
```

# VQT in TFQ

Author : Antonio J. Martinez

Contributors : Guillaume Verdon

Created : 2020-Feb-06

Last updated : 2020-Mar-06

[](https://colab.research.google.com/github/tensorflow/quantum/blob/research/vqt_qmhl/vqt_qmhl.ipynb)

In this notebook, you will explore the combination of quantum computing and classical energy-based models with TensorFlow Quantum.  The system under study is the [2D Heisenberg model](https://en.wikipedia.org/wiki/Heisenberg_model_(quantum)).  You will apply the Variational Quantum Thermalizer (VQT) to produce approximate thermal states of this model.  VQT was first proposed in the paper [here.](https://arxiv.org/abs/1910.02071)

## Install and import dependencies

```
!pip install --upgrade tensorflow==2.1.0
```

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

```
%%capture
import cirq
import itertools
import numpy as np
import random
from scipy import linalg
import seaborn
import sympy
import tensorflow as tf
import tensorflow_probability as tfp
import tensorflow_quantum as tfq

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

## 2D Heisenberg model

### Hamiltonian definition

This Hamiltonian is supported on a rectangular lattice of qubits:
$$\hat{H}_{\text{heis}} = \sum_{\langle ij\rangle_h} J_{h} \hat{S}_i \cdot \hat{S}_j +  \sum_{\langle ij\rangle_v} J_{v} \hat{S}_i \cdot \hat{S}_j,$$
where $h$ ($v$) denote horizontal (vertical) bonds, while $\langle \cdot \rangle $ represent nearest-neighbor pairings.
  You can build this Hamiltonian using Cirq `PauliString` and `PauliSum` objects:

```
def get_qubit_grid(rows, cols):
  """Rectangle of qubits returned as a nested list."""
  qubits = []
  for r in range(rows):
    qubits.append([])
    for c in range(cols):
      qubits[-1].append(cirq.GridQubit(r, c))
  return qubits

def get_bond(q0, q1):
  """Given two Cirq qubits, return the PauliSum that bonds them."""
  return cirq.PauliSum.from_pauli_strings([
    cirq.PauliString(cirq.X(q0), cirq.X(q1)),
    cirq.PauliString(cirq.Y(q0), cirq.Y(q1)),
    cirq.PauliString(cirq.Z(q0), cirq.Z(q1))])

def get_heisenberg_hamiltonian(qubits, jh, jv):
  """Returns the 2D Heisenberg Hamiltonian over the given grid of qubits."""
  heisenberg = cirq.PauliSum()
  # Apply horizontal bonds
  for r in qubits:
    for q0, q1 in zip(r, r[1::]):
      heisenberg += jh * get_bond(q0, q1)
  # Apply vertical bonds
  for r0, r1 in zip(qubits, qubits[1::]):
    for q0, q1 in zip(r0, r1):
      heisenberg += jv * get_bond(q0, q1)
  return heisenberg
```

For visualization and verification purposes, the following function recovers an explicit matrix from a Cirq `PauliSum` given a linear ordering of the qubits which support it:

```
def pauli_sum_to_matrix(qubits, pauli_sum):
  """Unpacks each pauli string in the pauli sum into a matrix and sums them."""
  matrix = np.zeros((2**len(qubits), 2**len(qubits)), dtype=np.complex128)
  for pauli_string in pauli_sum:
    coeff = pauli_string.coefficient
    bare_string = pauli_string/coeff
    matrix += coeff*bare_string.dense(qubits)._unitary_()
  return matrix
```

### Target density matrix

Here you define the parameters of the system to be learned.  The 2D Heisenberg model is defined by the number of rows and columns in the qubit lattice, the bond strengths in the horizontal and vertical directions, and the inverse temperature $\beta$.  Here, we use the same parameters as in the associated paper:

```
num_rows = 2
num_cols = 2
jh = 1
jv = 0.6
beta = 2.6

# Get the grid of qubits.
all_qubits = get_qubit_grid(num_rows, num_cols)
all_qubits_flat = [q for r in all_qubits for q in r]
```

Given a Hamiltonian $\hat{H}$ and an inverse temperature $\beta$, the thermal state $\rho_T$ is given by
$$\rho_T = e^{-\beta \hat{H}}.$$
Since our target system is small, you can compute this matrix exponential directly, using the `PauliSum`-to-matrix converter defined above:

```
num_H = pauli_sum_to_matrix(
    all_qubits_flat, get_heisenberg_hamiltonian(all_qubits, jh, jv))
heisenberg_exp = linalg.expm(-beta*num_H)
exact_thermal_state = np.true_divide(heisenberg_exp, np.trace(heisenberg_exp))
seaborn.heatmap(abs(exact_thermal_state))
```

Recall that any density matrix $\rho$ [can be written as](https://en.wikipedia.org/wiki/Density_matrix#Definition)
$$\rho = \sum_i p_i |\psi_i\rangle\langle\psi_i|,$$
where $|\psi_i\rangle$ is a pure state and $p_i$ is the classical probability of encoutering that state in the mixture.   Since TFQ is a pure state simulator, we will emulate density matrices by outputting pure states according to their probabilities $p_i$, which by the equation above is equivalent to outputting the full density matrix.  We define here a function that converts such a list of pure states into the associated density matrix:

```
def pure_state_list_to_density_matrix(pure_states):
  """Return the uniform mixture of the given list of pure states."""
  dim = len(pure_states[0].numpy())
  n_s = pure_states.shape[0]
  thermal_state = np.zeros((dim, dim), dtype=np.complex128)
  for i in range(n_s):
    psi = pure_states[i].numpy()
    thermal_state += np.outer(psi, psi)
  return np.true_divide(thermal_state, n_s)
```

Finally, to track the performance of our models, we need a measure of the distance of our estimated density matrix $\tilde{\rho}$ from the target density matrix $\rho_T$.  One common metric is the [fidelity](https://en.wikipedia.org/wiki/Fidelity_of_quantum_states), which is defined as
$$F(\tilde{\rho}, \rho_T) = \text{tr}\left[\sqrt{\sqrt{\tilde{\rho}}\rho_T\sqrt{\tilde{\rho}}}\right]^2.$$
This is tractable to compute because our model system is small.  Below we define a function that computes this quantity:

```
def fidelity(dm1, dm2):
  """Calculate the fidelity between the two given density matrices."""
  dm1_sqrt = linalg.sqrtm(dm1)
  return abs(np.trace(linalg.sqrtm(
      np.matmul(dm1_sqrt, np.matmul(dm2, dm1_sqrt))))) ** 2
```

## Energy based models

Energy based models are a type of machine learning ansatze inspired by physics and exponential families.  The advantage of using energy based models for probabilistic modeling is that fair samples can be drawn from the distributions they define without requiring computation of their partition functions.

One specific class of EBM is the Boltzmann machine.  The energy of a spin configuration $x \in \{-1, 1\}^n$ in this model is defined as:
$$E(x) = -\sum_{i, j}w_{ij} x_i x_j - \sum_i b_i x_i.$$

This classical model can be easily converted into a quantum mechanical Ising model by replacing each bit with the Pauli $Z$ operator, and considering the usual mapping of the spin to qubit pictures 1 -> $|0\rangle$ and $-1$ -> $|1\rangle$.

In the special case where the connection weights $w_{ij}$ are all zero, the Boltzmann machine is reduced to a product of independent Bernoulli distributions over the set of qubits.  This "Bernoulli EBM" has many simplifying properties, and hence you will explore this EBM first in the examples below.  Later in the notebook, you will apply the full Boltzmann EBM to VQT.

###  Energy functions

Here we define functions which compute the energy of a Boltzmann or Bernoulli EBM given the weight, biases, and bitstrings:

```
def bitstring_to_spin_config(bitstring):
  """Implements the mapping from the qubit to the spin picture."""
  return [-1 if b == 1 else 1 for b in bitstring]

def spin_config_to_bitstring(spin_config):
  """Implements the mapping from the spin to the qubit picture."""
  return [0 if s == 1 else 1 for s in spin_config]

def ebm_energy(spin_config, biases, weights=None):
  """Given a rank-2 tensor representing the weight matrix and a rank-1 tensor
  representing the biases, calculate the energy of the spin configuration."""
  energy = 0
  if weights is not None:
    for w_row, xi in zip(weights.numpy(), spin_config):
      for wij, xj in zip(w_row, spin_config):
        energy -= wij*xi*xj
  for bi, xi in zip(biases.numpy(), spin_config):
    energy -= bi*xi
  return energy

def ebm_energy_avg(spin_config_list, biases, weights=None):
  """Average energy over a set of spin configuration samples."""
  energy_avg = 0
  for spin_config in spin_config_list:
    energy_avg += ebm_energy(spin_config, biases, weights)
  energy_avg /= len(spin_config_list)
  return energy_avg
```

We also define functions which initialize TF Variables for our weights and biases.  Initializing all weights and biases near 0 means we begin near the uniform distribution, which can also be thought of as starting with a high temperature prior:

```
def get_initialized_ebm_biases(num_units):
  return tf.Variable(
      tf.random.uniform(minval=-0.1, maxval=0.1, shape=[num_units],
                        dtype=tf.float32), dtype=tf.float32)
def get_initialized_ebm_weights(num_units):
  return tf.Variable(
      tf.random.uniform(minval=-0.1, maxval=0.1,
                        shape=[num_units, num_units],dtype=tf.float32), dtype=tf.float32)
```

### EBM derivatives

The derivative of an EBM given a bitstring is easy to compute.  In fact, the derivatives are independent of the weights and biases:
$$\nabla_{w_{ij}}E(x) = -x_ix_j\quad \text{and}\quad \nabla_{b_{i}}E(x) = -x_i.$$
Information about the weights and biases enters by averaging these derivates over samples from the EBM.

```
def ebm_weights_derivative(spin_config):
  w_deriv = np.zeros((len(spin_config), len(spin_config)))
  for i, x_i in enumerate(spin_config):
    for j, x_j in enumerate(spin_config):
      w_deriv[i][j] = -x_i*x_j
  return w_deriv

def ebm_biases_derivative(spin_config):
  b_deriv = np.zeros(len(spin_config))
  for i, x_i in enumerate(spin_config):
    b_deriv[i] = -x_i
  return b_deriv

def ebm_weights_derivative_avg(spin_config_list):
  w_deriv = np.zeros((len(spin_config_list[0]), len(spin_config_list[0])))
  for spin_config in spin_config_list:
    w_deriv += ebm_weights_derivative(spin_config)
  return np.true_divide(w_deriv, len(spin_config_list))

def ebm_biases_derivative_avg(spin_config_list):
  b_deriv = np.zeros(len(spin_config_list[0]))
  for spin_config in spin_config_list:
    b_deriv += ebm_biases_derivative(spin_config)
  return np.true_divide(b_deriv, len(spin_config_list))
```

### Classical VQT loss gradients

As discussed in the paper, the gradient of the VQT loss function can be calculated without computing entropies or partition functions.  For example, the gradient of the VQT free energy loss with respect to the classical model parameters can be writtent as:
$$\partial_{\theta} \mathcal{L}_{\text{fe}} =\mathbb{E}_{x\sim p_{\theta}(x)}[(E_{\theta}(x)-\beta H_{\phi}(x) ) \nabla_{\theta}E_{\theta}(x) ]-(\mathbb{E}_{x\sim p_{\theta}(x)}[E_{\theta}(x)-\beta H_{\phi}(x)]) ( \mathbb{E}_{y\sim p_{\theta}(y)}[\nabla_{\theta}E_{\theta}(y)] ).$$
Below these gradients are defined for the general Boltzmann EBM.  In the VQT gradients, each entry in `bitstring_list` corresponds to the entry with the same index in `energy_losses`, where for each bitstring $x$, we compute the product $\beta\langle x|H|x\rangle$.  The list of bitstrings is assumed to be sampled from the EBM.

```
def get_vqt_weighted_weights_grad_product(
    energy_losses, spin_config_list, biases, weights):
  """Implements the first term in the derivative of the FE loss,
  for the weights of a Boltzmann EBM."""
  w_deriv = np.zeros((len(spin_config_list[0]), len(spin_config_list[0])))
  for e_loss, spin_config in zip(energy_losses, spin_config_list):
    w_deriv = w_deriv + (
        ebm_energy(spin_config, biases, weights) - e_loss
        )*ebm_weights_derivative(spin_config)
  return np.true_divide(w_deriv, len(energy_losses))

def get_vqt_weighted_biases_grad_product(
    energy_losses, spin_config_list, biases, weights=None):
  """Implements the first term in the derivative of the FE loss,
  for the biases of a Boltzmann EBM."""
  b_deriv = np.zeros(len(spin_config_list[0]))
  for e_loss, spin_config in zip(energy_losses, spin_config_list):
    b_deriv = b_deriv + (
        ebm_energy(spin_config, biases, weights) - e_loss
        )*ebm_biases_derivative(spin_config)
  return np.true_divide(b_deriv, len(energy_losses))

def get_vqt_factored_weights_grad_product(
    energy_losses, spin_config_list, biases, weights):
  """Implements the second term in the derivative of the FE loss,
  for the weights of a Boltzmann EBM."""
  energy_losses_avg = tf.reduce_mean(energy_losses)
  classical_energy_avg = ebm_energy_avg(spin_config_list, biases, weights)
  energy_diff_avg = classical_energy_avg - energy_losses_avg
  return energy_diff_avg*ebm_weights_derivative_avg(spin_config_list)

def get_vqt_factored_biases_grad_product(
    energy_losses, spin_config_list, biases, weights=None):
  """Implements the second term in the derivative of the FE loss,
  for the biases of a Boltzmann EBM."""
  energy_losses_avg = tf.reduce_mean(energy_losses)
  classical_energy_avg = ebm_energy_avg(spin_config_list, biases, weights)
  energy_diff_avg = classical_energy_avg - energy_losses_avg
  return energy_diff_avg*ebm_biases_derivative_avg(spin_config_list)
```

## Model components

### Ansatz unitary

The parameterized unitary ansatz you will use consists of alternating layers of general single qubit rotations and nearest-neighbor entangling gates:

```
def get_rotation_1q(q, a, b, c):
  """General single qubit rotation."""
  return cirq.Circuit(cirq.X(q) ** a, cirq.Y(q) ** b, cirq.Z(q) ** c)

def get_rotation_2q(q0, q1, a):
  """Exponent of entangling CNOT gate."""
  return cirq.Circuit(cirq.CNotPowGate(exponent=a)(q0, q1))

def get_layer_1q(qubits, layer_num, name):
  """Apply single qubit rotations to all the given qubits."""
  layer_symbols = []
  circuit = cirq.Circuit()
  for n, q in enumerate(qubits):
    a, b, c = sympy.symbols(
        "a{2}_{0}_{1} b{2}_{0}_{1} c{2}_{0}_{1}".format(layer_num, n, name))
    layer_symbols += [a, b, c]
    circuit += get_rotation_1q(q, a, b, c)
  return circuit, layer_symbols

def get_layer_2q(qubits, layer_num, name):
  """Apply CNOT gates to all pairs of nearest-neighbor qubits."""
  layer_symbols = []
  circuit = cirq.Circuit()
  for n, (q0, q1) in enumerate(zip(qubits[::2], qubits[1::2])):
    a = sympy.symbols("a{2}_{0}_{1}".format(layer_num, n, name))
    layer_symbols += [a]
    circuit += get_rotation_2q(q0, q1, a)
  shifted_qubits = qubits[1::]+[qubits[0]]
  for n, (q0, q1) in enumerate(zip(shifted_qubits[::2], shifted_qubits[1::2])):
    a = sympy.symbols("a{2}_{0}_{1}".format(layer_num, n+1, name))
    layer_symbols += [a]
    circuit += get_rotation_2q(q0, q1, a)
  return circuit, layer_symbols

def get_one_full_layer(qubits, layer_num, name):
  """Stack the one- and two-qubit parameterized circuits."""
  circuit = cirq.Circuit()
  all_symbols = []
  new_circ, new_symb = get_layer_1q(qubits, layer_num, name)
  circuit += new_circ
  all_symbols += new_symb
  new_circ, new_symb = get_layer_2q(qubits, layer_num + 1, name)
  circuit += new_circ
  all_symbols += new_symb
  return circuit, all_symbols

def get_model_unitary(qubits, num_layers, name=""):
  """Build our full parameterized model unitary."""
  circuit = cirq.Circuit()
  all_symbols = []
  for i in range(num_layers):
    new_circ, new_symb = get_one_full_layer(qubits, 2*i, name)
    circuit += new_circ
    all_symbols += new_symb
  return circuit, all_symbols
```

### Bitstring injector

You also need a way to feed bitstrings into the quantum model.  These bitstrings can be prepared by applying an X gate to every qubit that should be excited.  The following function returns a parameterized circuit which prepares any given bitstring:

```
def get_bitstring_circuit(qubits):
  """Returns wall of parameterized X gates and the bits used to turn them on."""
  circuit = cirq.Circuit()
  all_symbols = []
  for n, q in enumerate(qubits):
    new_bit = sympy.Symbol("bit_{}".format(n))
    circuit += cirq.X(q) ** new_bit
    all_symbols.append(new_bit)
  return circuit, all_symbols
```

## Factorized latent state

### Bernoulli EBM

The Bernoulli EBM can be used to parameterize a factorized latent state.  The probability of sampling a 1 from a unit with bias $b$ is:

$$p = \frac{e^b}{e^b + e^{-b}}$$

Since the units of a Bernoulli EBM are independent, the probability of a given spin configuration is simply the product of the individual unit probabilities:

$$p(x) = \prod_i\frac{e^{x_ib_i}}{e^{b_i} + e^{-b_i}}$$

This distribution is easy to sample from.

```
def bernoulli_spin_p1(b):
  return np.exp(b)/(np.exp(b) + np.exp(-b))

def sample_spins_bernoulli(num_samples, biases):
  prob_list = []
  for bias in biases.numpy():
    prob_list.append(bernoulli_spin_p1(bias))
  # The `probs` keyword specifies the probability of a 1 event
  latent_dist = tfp.distributions.Bernoulli(probs=prob_list, dtype=tf.float32)
  bit_samples = latent_dist.sample(num_samples).numpy()
  spin_samples = []
  for sample in bit_samples:
    spin_samples.append([])
    for bit in sample:
      if bit == 0:
        spin_samples[-1].append(-1)
      else:
        spin_samples[-1].append(1)
  return spin_samples
```

The entropy of a single unit with bias $b$ in our Bernoulli EBM is:

$S = \frac{be^{b} - be^{-b}}{e^{b} + e^{-b}}- \log[e^{b} + e^{-b}]$

For a factorized latent distribution, the entropy is simply the sum of the entropies of the individual factors.

```
def bernoulli_factor_partition(b):
  return np.exp(b) + np.exp(-b)

def bernoulli_partition(biases):
  partition = 1
  for bias in biases.numpy():
    partition *= bernoulli_factor_partition(bias)
  return partition

def bernoulli_factor_entropy(b):
  Z = bernoulli_factor_partition(b)
  return (b*np.exp(b) - b*np.exp(-b))/Z - np.log(Z)

def bernoulli_entropy(biases):
  entropy = 0
  for bias in biases.numpy():
    entropy += bernoulli_factor_entropy(bias)
  return entropy
```

Finally we define a function for converting the classical Bernoulli distribution into an Ising model whose expectation values can be simulated in the TFQ ops:

```
def bernoulli_ebm_to_ising(qubits, biases, bare=False):
  pauli_s_list = []
  for i, bi in enumerate(biases.numpy()):
    if bare:
      coeff = 1.0
    else:
      coeff = bi
    pauli_s_list.append(cirq.PauliString(coeff, cirq.Z(qubits[i])))
  if bare:
    return pauli_s_list
  return cirq.PauliSum.from_pauli_strings(pauli_s_list)
```

### VQT

Build and view our unitary model and set up the TFQ Expectation Op inputs:

```
# Number of bitstring samples from our classical model to average over
num_samples = 300

# Number of rotations-plus-entanglement layers to stack.
# Note that the depth required to reach a given fidelity increases depending on
# the temperature and Hamiltonian parameters.
num_layers = 4

# Build the model unitary and visible state circuits
U, model_symbols = get_model_unitary(all_qubits_flat, num_layers)
V, bit_symbols = get_bitstring_circuit(all_qubits_flat)
visible_state = tfq.convert_to_tensor([V + U])

# Make a copy of the visible state for each bitstring we will sample
tiled_visible_state = tf.tile(visible_state, [num_samples])

# Upconvert symbols to tensors
vqt_symbol_names = tf.identity(tf.convert_to_tensor(
    [str(s) for s in bit_symbols + model_symbols], dtype=tf.dtypes.string))

# Build and tile the Hamiltonian
H = get_heisenberg_hamiltonian(all_qubits, jh, jv)
tiled_H = tf.tile(tfq.convert_to_tensor([[H]]), [num_samples, 1])

# Get the expectation op with a differentiator attached
expectation = tfq.differentiators.ForwardDifference().generate_differentiable_op(
            analytic_op=tfq.get_expectation_op())

SVGCircuit(U)
```

We can use gradient descent on the model parameters thanks to TFQ, and our classical model parameters are tractable due to our use of an energy based model.  The factorized nature of our latent space also allows us to efficiently obtain a loss function.

```
optimizer = tf.keras.optimizers.Adam(learning_rate=0.03)

# Initialize our model variables
vqt_model_params = tf.Variable(
    tf.random.uniform(minval=-0.1, maxval=0.1,
                      shape=[len(model_symbols)], dtype=tf.float32),
    dtype=tf.float32)

# Keep track of metrics during training
vqt_loss_history = []
vqt_fidelity_history = []
vqt_model_params_history = []
vqt_bias_history = []
vqt_density_matrix_history = []

# Initialize our EBM variables
vqt_biases = get_initialized_ebm_biases(len(all_qubits_flat))

# The innermost training step, where gradients are taken and applied
def vqt_train_step():
  # Sample from our EBM
  spin_config_list = sample_spins_bernoulli(num_samples, vqt_biases)
  bitstring_list = [spin_config_to_bitstring(s) for s in spin_config_list]
  bitstring_tensor = tf.convert_to_tensor(bitstring_list, dtype=tf.float32)

  # Use the samples to find gradient of the loss w.r.t. model parameters.
  with tf.GradientTape() as tape:
    tiled_vqt_model_params = tf.tile([vqt_model_params], [num_samples, 1])
    sampled_expectations = expectation(
        tiled_visible_state,
        vqt_symbol_names,
        tf.concat([bitstring_tensor, tiled_vqt_model_params], 1),
        tiled_H)
    energy_losses = beta*sampled_expectations
    energy_losses_avg = tf.reduce_mean(energy_losses)
  vqt_model_gradients = tape.gradient(energy_losses_avg, [vqt_model_params])

  # Build the classical model gradients
  weighted_biases_grad = get_vqt_weighted_biases_grad_product(
    energy_losses, spin_config_list, vqt_biases)
  factored_biases_grad = get_vqt_factored_biases_grad_product(
    energy_losses, spin_config_list, vqt_biases)
  biases_grad = tf.subtract(weighted_biases_grad, factored_biases_grad)

  # Apply the gradients
  optimizer.apply_gradients(zip([vqt_model_gradients[0], biases_grad],
                                [vqt_model_params, vqt_biases]))

  # Sample pure states to build the current estimate of the density matrix
  many_states = tfq.layers.State()(
      tiled_visible_state,
      symbol_names=vqt_symbol_names,
      symbol_values=tf.concat([bitstring_tensor, tiled_vqt_model_params], 1)
  )
  vqt_density_matrix_history.append(pure_state_list_to_density_matrix(many_states))

  # Record the history
  vqt_loss_history.append((energy_losses_avg - bernoulli_entropy(vqt_biases)).numpy())
  vqt_fidelity_history.append(
      fidelity(vqt_density_matrix_history[-1], exact_thermal_state))
  vqt_model_params_history.append(vqt_model_params.numpy())
  vqt_bias_history.append(vqt_biases.numpy())

  print("Current loss:")
  print(vqt_loss_history[-1])
  print("Current fidelity to optimal state:")
  print(vqt_fidelity_history[-1])
  print("Current estimated density matrix:")
  plt.figure()
  seaborn.heatmap(abs(vqt_density_matrix_history[-1]))
  plt.show()
```

With setup complete, we can now optimize our Heisenberg VQT.

```
def vqt_train(epochs):
  for epoch in range(epochs):
    vqt_train_step()
    print ('Epoch {} finished'.format(epoch + 1))

vqt_train(100)
```

We plot our metrics and visualize the motion of the parameters during training:

```
plt.plot(vqt_loss_history)
plt.xlabel('Epoch #')
plt.ylabel('Loss [free energy]')
```

```
plt.plot(vqt_fidelity_history)
plt.xlabel('Epoch #')
plt.ylabel('Fidelity with exact state')
```

## Classically correlated latent state

### Boltzmann machine EBM

The Bernoulli distribution is only able to inject entropy into our density matrix.  To encode classical correlations, we need to move beyond a factorized latent state.  This can be accomplished by allowing the weights of our Boltzmann machine to be non-zero.

Now that there are correlations, sampling from the model becomes non-trivial.  The probability of bitstring $x$ is:

$P(x) = \frac{\exp(-E(x))}{\sum_{y\in\{-1, 1\}^n} \exp(-E(y))}$

In general this function is intractable to compute directly; however, we can still obtain samples from the distribution efficiently. Markov chain Monte Carlo (MCMC) is one family of procedures for this efficient sampling.  Here, we use the simplest example of MCMC, the [Metropolis-Hastings](https://en.wikipedia.org/wiki/Metropolis%E2%80%93Hastings_algorithm) algorithm:

```
def make_proposal(y):
  """Flip spins in y to generate a new sample."""
  coin = tfp.distributions.Bernoulli(probs=[0.75]*len(y))
  samples = coin.sample(1).numpy()[0]
  x = []
  for s_i, y_i in zip(samples, y):
    if s_i:
      x.append(y_i)
    else:
      if y_i == 1:
        x.append(-1)
      else:
        x.append(1)
  return x

def sample_boltzmann(burn_in, num_samples, skip, initial_state, biases, weights):
  """Walk towards and sample from regions of high probability."""
  current_state = initial_state
  all_samples = []
  for i in range(burn_in + skip*num_samples):
    proposal = make_proposal(current_state)
    proposal_energy = ebm_energy(proposal, biases, weights)
    current_energy = ebm_energy(current_state, biases, weights)
    acceptance = min(np.exp(-proposal_energy)/np.exp(-current_energy), 1)
    threshold = random.random()
    if threshold <= acceptance:
      current_state = proposal
    if i >= burn_in:
      if (i - burn_in)%skip == 0:
        all_samples.append(current_state)
  return all_samples
```

Since there are now correlations between the bits, the partition function and entropy can no longer be computed in a scalable way.  However, for the small system size considered here, these quantities can be illustrative.

```
def boltzmann_partition(biases, weights):
  partition_value = 0
  for spin_config in itertools.product([-1, 1], repeat=biases.shape[0]):
    partition_value += np.exp(-ebm_energy(spin_config, biases, weights))
  return partition_value

def boltzmann_entropy(biases, weights):
  Z = boltzmann_partition(biases, weights)
  Z_log = np.log(Z)
  unnormalized = 0
  for spin_config in itertools.product([-1, 1], repeat=biases.shape[0]):
    this_energy = ebm_energy(spin_config, biases, weights)
    unnormalized += np.exp(-this_energy)*(-this_energy - Z_log)
  return -unnormalized/Z
```

Finally we define a function for converting the classical Boltzmann machine into an Ising model whose expectation values can be simulated in the TFQ ops:

```
def boltzmann_ebm_to_ising(qubits, biases, weights, bare=False):
  pauli_s_list = []
  for i, w_row in enumerate(weights.numpy()):
    for j, wij in enumerate(w_row):
      init_list = [cirq.Z(q) for qi, q in enumerate(qubits) if qi == i or qi == j]
      if bare:
        coeff = 1.0
      else:
        coeff = wij
      pauli_s_list.append(cirq.PauliString(coeff, init_list))
  for i, bi in enumerate(biases.numpy()):
    if bare:
      coeff = 1.0
    else:
      coeff = bi
    pauli_s_list.append(cirq.PauliString(coeff, cirq.Z(qubits[i])))
  if bare:
    return pauli_s_list
  return cirq.PauliSum.from_pauli_strings(pauli_s_list)
```

### VQT

```
# Initialize our model variables
vqt_model_params = tf.Variable(
    tf.random.uniform(minval=-0.1, maxval=0.1, shape=[len(model_symbols)],
                      dtype=tf.float32), dtype=tf.float32)

# Define the learning hyperparameters
burn_in = 100
skip = 7
optimizer = tf.keras.optimizers.Adam(learning_rate=0.025)

# Keep track of metrics during training
vqt_loss_history = []
vqt_fidelity_history = []
vqt_model_params_history = []
vqt_weights_history = []
vqt_bias_history = []
vqt_density_matrix_history = []

# Initialize our EBM variables
vqt_weights = get_initialized_ebm_weights(len(all_qubits_flat))
vqt_biases = get_initialized_ebm_biases(len(all_qubits_flat))

# The innermost training step, where gradients are taken and applied
def vqt_train_step():
  # Sample from our EBM
  spin_config_list = sample_boltzmann(burn_in, num_samples, skip,
                                      vqt_train_step.initial_state,
                                      vqt_biases, vqt_weights)
  vqt_train_step.initial_state = spin_config_list[-1]
  bitstring_list = [spin_config_to_bitstring(s) for s in spin_config_list]
  bitstring_tensor = tf.convert_to_tensor(bitstring_list, dtype=tf.float32)

  # Use the samples to find gradient of the loss w.r.t. model parameters.
  with tf.GradientTape() as tape:
    tiled_vqt_model_params = tf.tile([vqt_model_params], [num_samples, 1])
    sampled_expectations = expectation(
        tiled_visible_state,
        vqt_symbol_names,
        tf.concat([bitstring_tensor, tiled_vqt_model_params], 1),
        tiled_H)
    energy_losses = beta*sampled_expectations
    energy_losses_avg = tf.reduce_mean(energy_losses)
  vqt_model_gradients = tape.gradient(energy_losses_avg, [vqt_model_params])

  # Build the classical model gradients
  weighted_biases_grad = get_vqt_weighted_biases_grad_product(
    energy_losses, spin_config_list, vqt_biases, vqt_weights)
  factored_biases_grad = get_vqt_factored_biases_grad_product(
    energy_losses, spin_config_list, vqt_biases, vqt_weights)
  biases_grad = tf.subtract(weighted_biases_grad, factored_biases_grad)
  weighted_weights_grad = get_vqt_weighted_weights_grad_product(
    energy_losses, spin_config_list, vqt_weights, vqt_weights)
  factored_weights_grad = get_vqt_factored_weights_grad_product(
    energy_losses, spin_config_list, vqt_weights, vqt_weights)
  weights_grad = tf.subtract(weighted_weights_grad, factored_weights_grad)

  # Apply the gradients
  optimizer.apply_gradients(
      zip([vqt_model_gradients[0], weights_grad, biases_grad],
          [vqt_model_params, vqt_weights, vqt_biases]))

  # Sample pure states to build the current estimate of the density matrix
  many_states = tfq.layers.State()(
      tiled_visible_state,
      symbol_names=vqt_symbol_names,
      symbol_values=tf.concat([bitstring_tensor, tiled_vqt_model_params], 1)
  )
  vqt_density_matrix_history.append(pure_state_list_to_density_matrix(many_states))

  # Record the history
  vqt_loss_history.append(
      (energy_losses_avg - boltzmann_entropy(vqt_biases, vqt_weights)).numpy())
  vqt_fidelity_history.append(
      fidelity(vqt_density_matrix_history[-1], exact_thermal_state))
  vqt_model_params_history.append(vqt_model_params.numpy())
  vqt_weights_history.append(vqt_weights.numpy())
  vqt_bias_history.append(vqt_biases.numpy())

  print("Current loss:")
  print(vqt_loss_history[-1])
  print("Current fidelity to optimal state:")
  print(vqt_fidelity_history[-1])
  print("Current estimated density matrix:")
  plt.figure()
  seaborn.heatmap(abs(vqt_density_matrix_history[-1]))
  plt.show()

vqt_train_step.initial_state = [1]*len(bit_symbols)
```

```
def vqt_train(epochs):
  for epoch in range(epochs):
    vqt_train_step()
    print ('Epoch {} finished'.format(epoch + 1))

vqt_train(100)
```

```
plt.plot(vqt_loss_history)
plt.xlabel('Epoch #')
plt.ylabel('Loss [free energy]')
```

```
plt.plot(vqt_fidelity_history)
plt.xlabel('Epoch #')
plt.ylabel('Fidelity with exact state')
```

```

```