# Imports and Simulation Parameters

```python
import numpy as np
import math
import cmath
import scipy
import scipy.integrate
import sys

import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline

hbar = 1.0 / (2.0 * np.pi)

ZERO_TOLERANCE = 10**-6
```

    /Users/joseph/anaconda/lib/python2.7/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
      warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')

```python
MAX_VIBRATIONAL_STATES = 200

STARTING_GROUND_STATES = 5
STARTING_EXCITED_STATES = 5

time_scale_set = 10  #will divide the highest energy to give us the time step
low_frequency_cycles = 25.0 #will multiply the frequency of the lowest frequency mode to get the max time
```

```python
#See if a factorial_Storage dictionary exists already and if not, create one
try:
    a = factorial_storage
except:
    factorial_storage = {}
```

# Defining Parameters of the System

```python
energy_g = 0
energy_gamma = .1

energy_e = 0
energy_epsilon = .31

Huang_Rhys_Parameter = .80
S = Huang_Rhys_Parameter

#bookkeeping
overlap_storage = {}
electronic_energy_gap = energy_e + .5*energy_epsilon - (energy_g  + .5 * energy_gamma)
min_energy = energy_g + energy_gamma * .5

```

```python
mu_0 = 1.0
```

If we set the central frequency of a pulse at the 0->0 transition, and we decide we care about the ratio of the 0->1 transition to the 0->0 transition and set that to be $\tau$ then the desired pulse width will be
\begin{align}
    \sigma &= \sqrt{-\frac{2 \ln (\tau)}{\omega_{\epsilon}^2}}
\end{align}

```python
def blank_wavefunction(number_ground_states, number_excited_states):
    return np.zeros((number_ground_states + number_excited_states))

def perturbing_function(time):
#     stdev = 30000.0 * dt #very specific to 0->0 transition!
    stdev = 3000.0 * dt #clearly has a small amount of amplitude on 0->1 transition
    center = 6 * stdev
    return np.cos(electronic_energy_gap*(time - center) / hbar)*np.exp( - (time - center)**2 / (2 * stdev**2)) / stdev

def time_function_handle_from_tau(tau_proportion):
    stdev = np.sqrt( -2.0 * np.log(tau_proportion) / (energy_epsilon/hbar)**2)
    center = 6 * stdev
    return center, stdev, lambda t: np.cos(electronic_energy_gap*(t - center) / hbar)*np.exp( - (t - center)**2 / (2 * stdev**2)) / stdev

def time_function_handle_from_tau_and_kappa(tau_proportion, kappa_proportion):
    stdev = np.sqrt( -2.0 * np.log(tau_proportion) / (energy_epsilon/hbar)**2)
    center = 6 * stdev
    return center, stdev, lambda t: kappa_proportion * energy_gamma * np.cos(electronic_energy_gap*(t - center) / hbar)*np.exp( - (t - center)**2 / (2 * stdev**2)) / stdev

def perturbing_function_define_tau(time, tau_proportion):
    center, stdev, f = time_function_handle_from_tau(tau_proportion)
    return f(time)
```

```python

```

```python

```

# Defining Useful functions

$ O_{m}^{n} = \left(-1\right)^{n} \sqrt{\frac{e^{-S}S^{m+n}}{m!n!}} \sum_{j=0}^{\min \left( m,n \right)} \frac{m!n!}{j!(m-j)!(n-j)!}(-1)^j S^{-j} $

```python
def factorial(i):
    if i in factorial_storage:
        return factorial_storage[i]
    if i <= 1:
        return 1.0
    else:
        out =  factorial(i - 1) * i
        factorial_storage[i] = out
        return out

def ndarray_factorial(i_array):
    return np.array([factorial(i) for i in i_array])
```

```python
def overlap_function(ground_quantum_number, excited_quantum_number):
    m = ground_quantum_number
    n = excited_quantum_number
    if (m,n) in overlap_storage:
        return overlap_storage[(m,n)]
    output = (-1)**n
    output *= math.sqrt(math.exp(-S) * S**(m + n) /(factorial(m) * factorial(n)) )
    j_indeces = np.array(range(0, min(m,n) + 1))
    j_summation = factorial(m) * factorial(n) * np.power(-1.0, j_indeces) * np.power(S, -j_indeces)
    j_summation = j_summation / (ndarray_factorial(j_indeces) * ndarray_factorial( m - j_indeces) * ndarray_factorial(n - j_indeces) )
    output *= np.sum(j_summation)
    overlap_storage[(m,n)] = output
    return output
```

# Solving the Differential Equation

\begin{align*}
	\left(\frac{d G_a(t)}{dt} + \frac{i}{\hbar}\Omega_{(a)}\right) &=-E(t)\frac{i}{\hbar}  \sum_{b} E_b(t) \mu_{a}^{b}\\
	\left(\frac{d E_b(t)}{dt} + \frac{i}{\hbar} \Omega^{(b)} \right) &=-E(t)\frac{i}{\hbar} \sum_{a} G_a(t) \mu_{a}^{b}
\end{align*}
Or in a more compact form:
\begin{align*}
	\frac{d}{dt}\begin{bmatrix}
		G_a(t) \\
		E_b(t)
	\end{bmatrix}
	= -\frac{i}{\hbar}
	\begin{bmatrix}
		\Omega_{(a)} & E(t) \mu_{a}^{b} \\
		E(t) \mu_{a}^{b} & \Omega^{b}
	\end{bmatrix}
	\cdot
	\begin{bmatrix}
		G_a(t) \\
		E_b(t)
	\end{bmatrix}
\end{align*}

```python
def ode_diagonal_matrix(number_ground_states, number_excited_states):
    #Define the Matrix on the RHS of the above equation
    ODE_DIAGONAL_MATRIX = np.zeros((number_ground_states + number_excited_states, number_ground_states + number_excited_states), dtype=np.complex)
    #set the diagonals
    for ground_i in range(number_ground_states):
        ODE_DIAGONAL_MATRIX[ground_i, ground_i] = -1.0j * (energy_g + energy_gamma * (ground_i + .5)) / hbar

    for excited_i in range(number_excited_states):
        excited_index = excited_i + number_ground_states #the offset since the excited states comes later
        ODE_DIAGONAL_MATRIX[excited_index, excited_index] = -1.0j * (energy_e + energy_epsilon * (excited_i + .5)) / hbar

    return ODE_DIAGONAL_MATRIX

#now for the off-diagonals
def mu_matrix(c, number_ground_states, number_excited_states):
    MU_MATRIX = np.zeros((number_ground_states, number_excited_states), dtype = np.complex)
    for ground_a in range(number_ground_states):
        for excited_b in range(number_excited_states):
            new_mu_entry = overlap_function(ground_a, excited_b)
            if ground_a >0:
                new_mu_entry += c * math.sqrt(ground_a) * overlap_function(ground_a - 1, excited_b)

            new_mu_entry += c * math.sqrt(ground_a+1) * overlap_function(ground_a + 1, excited_b)

            MU_MATRIX[ground_a, excited_b] = new_mu_entry
    return MU_MATRIX

def ode_off_diagonal_matrix(c_value, number_ground_states, number_excited_states):
    output  = np.zeros((number_ground_states + number_excited_states, number_ground_states + number_excited_states), dtype=np.complex)
    MU_MATRIX = mu_matrix(c_value, number_ground_states, number_excited_states)
    output[0:number_ground_states, number_ground_states:] = -1.0j * mu_0 * MU_MATRIX  / hbar
    output[number_ground_states:, 0:number_ground_states] = -1.0j * mu_0 * MU_MATRIX.T  / hbar
    return output

def IR_transition_dipoles(number_ground_states, number_excited_states):
    "outputs matrices to calculate ground and excited state IR emission spectra.  Can be combined for total"
    output_g = np.zeros((number_ground_states + number_excited_states, number_ground_states + number_excited_states), dtype=np.complex)
    output_e = np.zeros((number_ground_states + number_excited_states, number_ground_states + number_excited_states), dtype=np.complex)

    for ground_a in range(number_ground_states):
        try:
            output_g[ground_a, ground_a + 1] = math.sqrt(ground_a + 1)
            output_g[ground_a + 1, ground_a] = math.sqrt(ground_a + 1)
        except:
            pass

        try:
            output_g[ground_a, ground_a - 1] = math.sqrt(ground_a)
            output_g[ground_a - 1, ground_a] = math.sqrt(ground_a)
        except:
            pass

    for excited_a in range(number_excited_states):
        matrix_index_e = number_ground_states + excited_a -1 #because of how 'number_ground_states' is defined
        try:
            output_e[matrix_index_e, matrix_index_e + 1] = math.sqrt(excited_a + 1)
            output_e[matrix_index_e + 1, matrix_index_e] = math.sqrt(excited_a + 1)
        except:
            pass

        try:
            output_e[matrix_index_e, matrix_index_e - 1] = math.sqrt(excited_a)
            output_e[matrix_index_e - 1, matrix_index_e] = math.sqrt(excited_a)
        except:
            pass

    return output_g, output_e
```

\begin{align*}
	\mu(x) &= \mu_0 \left(1 + \lambda x \right) \\
	&= \mu_0 \left(1 + c\left(a + a^{\dagger} \right) \right) \\
	\mu_{a}^{b} &= \mu_0\left(O_{a}^{b}  + c\left(\sqrt{a}O_{a-1}^{b} + \sqrt{a+1}O_{a+1}^{b}\right) \right)
\end{align*}

```python

```

```python
class VibrationalStateOverFlowException(Exception):
    def __init__(self):
        pass
```

```python

```

```python
def propagate_amplitude_to_end_of_perturbation(c_value, ratio_01_00, kappa=1, starting_ground_states=STARTING_GROUND_STATES, starting_excited_states=STARTING_EXCITED_STATES):
    center_time, stdev, time_function = time_function_handle_from_tau_and_kappa(ratio_01_00, kappa)
    ending_time = center_time + 8.0 * stdev

    number_ground_states = starting_ground_states
    number_excited_states = starting_excited_states

    while number_excited_states + number_ground_states < MAX_VIBRATIONAL_STATES:

        #define time scales
        max_energy = energy_e + energy_epsilon * (.5 + number_excited_states) + kappa * energy_gamma * mu_0
        dt = 1.0 / (time_scale_set *  max_energy)

        ODE_DIAGONAL = ode_diagonal_matrix(number_ground_states, number_excited_states)
        ODE_OFF_DIAGONAL = ode_off_diagonal_matrix(c_value, number_ground_states, number_excited_states)

        def ODE_integrable_function(time, coefficient_vector):
            ODE_TOTAL_MATRIX = ODE_OFF_DIAGONAL * time_function(time) + ODE_DIAGONAL
            return np.dot(ODE_TOTAL_MATRIX, coefficient_vector)

        #define the starting wavefuntion
        initial_conditions = blank_wavefunction(number_ground_states, number_excited_states)
        initial_conditions[0] = 1

        #create ode solver
        current_time = 0.0
        ode_solver = scipy.integrate.complex_ode(ODE_integrable_function)
        ode_solver.set_initial_value(initial_conditions, current_time)

        #Run it
        results = []
        try:  #this block catches an overflow into the highest ground or excited vibrational state
            while current_time < ending_time:
#                 print(current_time, ZERO_TOLERANCE)
                #update time, perform solution
                current_time = ode_solver.t+dt
                new_result = ode_solver.integrate(current_time)
                results.append(new_result)
                #make sure solver was successful
                if not ode_solver.successful():
                    raise Exception("ODE Solve Failed!")
                #make sure that there hasn't been substantial leakage to the highest excited states
                re_start_calculation = False
                if abs(new_result[number_ground_states - 1])**2 >= ZERO_TOLERANCE:
                    number_ground_states +=1
#                     print("Increasing Number of Ground vibrational states to %i " % number_ground_states)
                    re_start_calculation = True
                if abs(new_result[-1])**2 >= ZERO_TOLERANCE:
                    number_excited_states +=1
#                     print("Increasing Number of excited vibrational states to %i " % number_excited_states)
                    re_start_calculation = True
                if re_start_calculation:
                    raise VibrationalStateOverFlowException()
        except VibrationalStateOverFlowException:
            #Move on and re-start the calculation
            continue

        #Finish calculating
        results = np.array(results)
        return results, number_ground_states, number_excited_states
    raise Exception("NEEDED TOO MANY VIBRATIONAL STATES!  RE-RUN WITH DIFFERENT PARAMETERS!")
```

```python
def get_average_quantum_number_time_series(c_value, ratio_01_00, kappa=1, starting_ground_states=STARTING_GROUND_STATES, starting_excited_states=STARTING_EXCITED_STATES):
    results, number_ground_states, number_excited_states = propagate_amplitude_to_end_of_perturbation(c_value, ratio_01_00, kappa, starting_ground_states, starting_excited_states)
    probabilities = np.abs(results)**2
    #calculate the average_vibrational_quantum_number series
    average_ground_quantum_number = probabilities[:,0:number_ground_states].dot(np.array(range(number_ground_states)) )
    average_excited_quantum_number = probabilities[:,number_ground_states:].dot(np.array(range(number_excited_states)))
    return average_ground_quantum_number, average_excited_quantum_number, results, number_ground_states, number_excited_states
```

```python
def IR_emission_spectrum_after_excitation(c_value, ratio_01_00, kappa=1, starting_ground_states=STARTING_GROUND_STATES, starting_excited_states=STARTING_EXCITED_STATES):
    center_time, stdev, time_function = time_function_handle_from_tau_and_kappa(ratio_01_00, kappa)

    perturbation_ending_time = center_time + 8.0 * stdev
    simulation_ending_time = perturbation_ending_time + low_frequency_cycles * hbar/min_energy

    number_ground_states = starting_ground_states
    number_excited_states = starting_excited_states

    while number_excited_states + number_ground_states < MAX_VIBRATIONAL_STATES:

        ir_transDipole_g, ir_transDipole_e = IR_transition_dipoles(number_ground_states, number_excited_states)

        time_emission_g = [0]
        time_emission_e = [0]

        #define time scales
        e = energy_e + energy_epsilon * (.5 + number_excited_states)
        g = energy_g + energy_gamma* (.5 + number_ground_states)
        plus = e + g
        minus = e - g
        J = kappa * energy_gamma * mu_0
        max_split_energy = plus + math.sqrt(minus**2 + 4 * J**2)
        max_energy = max_split_energy * .5
        dt = 1.0 / (time_scale_set *  max_energy)

        time_values = np.arange(0, simulation_ending_time, dt)

        ODE_DIAGONAL = ode_diagonal_matrix(number_ground_states, number_excited_states)
        ODE_OFF_DIAGONAL = ode_off_diagonal_matrix(c_value, number_ground_states, number_excited_states)

        def ODE_integrable_function(time, coefficient_vector):
            ODE_TOTAL_MATRIX = ODE_OFF_DIAGONAL * time_function(time) + ODE_DIAGONAL
            return np.dot(ODE_TOTAL_MATRIX, coefficient_vector)

        def ODE_jacobean(time, coefficient_vector):
            ODE_TOTAL_MATRIX = ODE_OFF_DIAGONAL * time_function(time) + ODE_DIAGONAL
            return ODE_TOTAL_MATRIX

        #define the starting wavefuntion
        initial_conditions = blank_wavefunction(number_ground_states, number_excited_states)
        initial_conditions[0] = 1

        #create ode solver
        current_time = 0.0
        try:
            del ode_solver
        except:
            pass
#         ode_solver = scipy.integrate.complex_ode(ODE_integrable_function)
        ode_solver = scipy.integrate.complex_ode(ODE_integrable_function, jac = ODE_jacobean)
#         ode_solver.set_integrator("lsoda")
        ode_solver.set_integrator("vode", with_jacobian=True)

        ode_solver.set_initial_value(initial_conditions, current_time)

        #Run it
        results = []
        try:  #this block catches an overflow into the highest ground or excited vibrational state
            while current_time < simulation_ending_time:
#                 print(current_time, ZERO_TOLERANCE)
                #update time, perform solution
                current_time = ode_solver.t+dt
                new_result = ode_solver.integrate(current_time)
                results.append(new_result)
                #make sure solver was successful
                if not ode_solver.successful():
                    raise Exception("ODE Solve Failed!")

                if current_time < perturbation_ending_time:
                    #make sure that there hasn't been substantial leakage to the highest excited states
                    re_start_calculation = False
                    if abs(new_result[number_ground_states - 1])**2 >= ZERO_TOLERANCE:
                        number_ground_states +=1
#                         print("Increasing Number of Ground vibrational states to %i " % number_ground_states)
                        re_start_calculation = True
                    if abs(new_result[-1])**2 >= ZERO_TOLERANCE:
                        number_excited_states +=1
#                         print("Increasing Number of excited vibrational states to %i " % number_excited_states)
                        re_start_calculation = True
                    if re_start_calculation:
                        raise VibrationalStateOverFlowException()
                #calculate IR emission
                time_emission_g.append(np.conj(new_result).T.dot(ir_transDipole_g.dot(new_result)))
                time_emission_e.append(np.conj(new_result).T.dot(ir_transDipole_e.dot(new_result)))
                #on to next time value...

        except VibrationalStateOverFlowException:
            #Move on and re-start the calculation
            continue

        #Finish calculating
        results = np.array(results)
        n_t = len(time_emission_e)

        time_emission_g = np.array(time_emission_g)
        time_emission_e = np.array(time_emission_e)

        filter_x = np.array(range(n_t))
        filter_center = n_t / 2.0
        filter_sigma = n_t / 10.0
        filter_values = np.exp(-(filter_x - filter_center)**2 / (2 * filter_sigma**2))

        frequencies = np.fft.fftshift(np.fft.fftfreq(time_emission_g.shape[0], d= dt))
        frequency_emission_g = dt * np.fft.fftshift(np.fft.fft(time_emission_g * filter_values))
        frequency_emission_e = dt * np.fft.fftshift(np.fft.fft(time_emission_e * filter_values))

        return results, frequencies, frequency_emission_g, frequency_emission_e, number_ground_states, number_excited_states
    raise Exception("NEEDED TOO MANY VIBRATIONAL STATES!  RE-RUN WITH DIFFERENT PARAMETERS!")
```

```python
c_values = np.logspace(-4, np.log10(1), 5)
tau_values = np.logspace(-4, np.log10(.95), 10)
kappa_values = np.logspace(-2, np.log10(5), 20)

number_calcs = c_values.shape[0] * tau_values.shape[0] * kappa_values.shape[0]

heating_results_ground = np.zeros((kappa_values.shape[0],  tau_values.shape[0], c_values.shape[0]))
ir_amplitudes = np.zeros(heating_results_ground.shape)
heating_results_excited = np.zeros(heating_results_ground.shape)

# Keep track of the IR Spectrum

n_g = STARTING_GROUND_STATES
n_e = STARTING_EXCITED_STATES

counter = 1
# we will use the value of c as a bellweather for how many starting states to work with.
c_to_ng = {}
c_to_ne = {}
for i_kappa, kappa in enumerate(kappa_values):
    # as we increase in both tau and
    for i_tau, tau in enumerate(tau_values):

        for i_c, c in enumerate(c_values):
            try:
                n_g = c_to_ng[c]
                n_e = c_to_ne[c]
            except:
                n_g = STARTING_GROUND_STATES
                n_e = STARTING_EXCITED_STATES
                c_to_ng[c] = n_g
                c_to_ne[c] = n_e

            sys.stdout.flush()
            sys.stdout.write("\r%i / %i Calculating kappa=%f, c=%f, tau=%f at n_g = %i and n_e=%i..." %(counter, number_calcs, kappa, c, tau, n_g, n_e))
#             print("\r%i / %i Calculating kappa=%f, c=%f, tau=%f at n_g = %i and n_e=%i..." %(counter, number_calcs, kappa, c, tau, n_g, n_e))
#             n_bar_g, n_bar_e, results, num_g, num_e = get_average_quantum_number_time_series(c,
#                                                                                tau,
#                                                                                kappa,
#                                                                                starting_ground_states = n_g,
#                                                                                starting_excited_states = n_e)

#             heating_results_ground[i_kappa, i_tau, i_c] = n_bar_g[-1]
#             heating_results_excited[i_kappa, i_tau, i_c] = n_bar_e[-1]

            _, frequencies, emission_g, emission_e, num_g, num_e = IR_emission_spectrum_after_excitation(c,
                                                                                tau,
                                                                                kappa,
                                                                                starting_ground_states = n_g,
                                                                                starting_excited_states = n_e)
            if num_g > c_to_ng[c]:
                c_to_ng[c] = num_g
            if num_e > c_to_ne[c]:
                c_to_ne[c] = num_e
            vibrational_frequency_index = np.argmin(np.abs(energy_gamma - frequencies))
            ir_power = np.abs(emission_g[vibrational_frequency_index])**2
            ir_amplitudes[i_kappa, i_tau, i_c] = ir_power
            counter +=1
#             plt.figure()
#             plt.title(r"$\kappa{}, \tau={}, c={}".format(kappa, tau, c))
#             plt.plot(frequencies, emission_g)
#             plt.xlim(0, 2)

```

    1000 / 1000 Calculating kappa=5.000000, c=1.000000, tau=0.950000 at n_g = 67 and n_e=52...

```python

```

```python
# decreased dt, does that keep these parameters from failing? last one is 600 / 800 Calculating kappa=8.858668, c=0.750000, tau=0.850000
for i_kappa, kappa in enumerate(kappa_values):
    for i_tau, tau in enumerate(tau_values):
        ir_power = ir_amplitudes[i_kappa, i_tau, :]
        plt.loglog(c_values, ir_power, "*-")
plt.xlabel(r"$c$")
plt.figure()
for i_kappa, kappa in enumerate(kappa_values):
    for i_c, c in enumerate(c_values):
        ir_power = ir_amplitudes[i_kappa, :, i_c]
        plt.loglog(tau_values, ir_power, "*-")
plt.xlabel(r"$\tau$")
plt.figure()
for i_tau, tau in enumerate(tau_values):
    for i_c, c in enumerate(c_values):
#     for i_c in [0,-1]:
        ir_power = ir_amplitudes[:, i_tau, i_c]
#         plt.loglog(kappa_values, ir_power, ["blue", "red"][i_c])
        plt.loglog(kappa_values, ir_power)
plt.xlabel(r"$\kappa$")
# plt.xlim(-.1, 1.1)
# plt.ylim(-10, 500)
```

```python
c_log = np.log10(c_values)
tau_log = np.log10(tau_values)
kappa_log = np.log10(kappa_values)
log_ir_amplitudes = np.log(ir_amplitudes)
num_levels = 100
contours = np.linspace(np.min(log_ir_amplitudes), np.max(log_ir_amplitudes), num_levels)
```

```python

# for i_kappa, kappa in enumerate(kappa_values):
#     ir_power = log_ir_amplitudes[i_kappa, :, :]
#     plt.figure()
#     plt.contourf(c_log, tau_log, ir_power, contours)
#     plt.title(r"$\kappa = {}$".format(kappa))
#     plt.ylabel(r"$c$")
#     plt.xlabel(r"$\tau$")
#     plt.colorbar()

# for i_tau, tau in enumerate(tau_values):
#     ir_power = log_ir_amplitudes[:, i_tau, :]
#     plt.figure()
#     plt.contourf(c_log, kappa_log, ir_power, contours)
#     plt.title(r"$\tau = {}$".format(tau))
#     plt.ylabel(r"$\kappa$")
#     plt.xlabel(r"$c$")
#     plt.colorbar()

for i_c, c in enumerate(c_values):
    ir_power = log_ir_amplitudes[:, :, i_c]
    plt.figure()
    plt.contourf(tau_log, kappa_log, ir_power, contours)
    plt.title(r"$\tau = {}$".format(tau))
    plt.ylabel(r"$\kappa$")
    plt.xlabel(r"$c$")
    plt.colorbar()

```

```python
for i_c, c in enumerate(c_values):
    ir_power = log_ir_amplitudes[:, :, i_c]
    plt.figure()
    plt.contourf(tau_log, kappa_log, ir_power, contours)
    plt.title(r"$c = {}$".format(c))
    plt.ylabel(r"$\kappa$")
    plt.xlabel(r"$\tau$")
    plt.colorbar()
    plt.savefig("{}_expProtocol.png".format(c))
```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python
np.linspace()
```


# ADM Quantities in terms of BSSN Quantities

## Author: Zach Etienne
### Formatting improvements courtesy Brandon Clark

[comment]: <> (Abstract: TODO)

**Module Status:**  Self-Validated

**Validation Notes:** This tutorial module has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). **Additional validation tests may have been performed, but are as yet, undocumented. (TODO)**

### NRPy+ Source Code for this module: [ADM_in_terms_of_BSSN.py](../edit/BSSN/ADM_in_terms_of_BSSN.py)

## Introduction:
This tutorial module constructs all quantities in the [ADM formalism](https://en.wikipedia.org/wiki/ADM_formalism) (see also Chapter 2 in Baumgarte & Shapiro's book *Numerical Relativity*) in terms of quantities in our adopted (covariant, tensor-rescaled) BSSN formalism. That is to say, we will write the ADM quantities $\left\{\gamma_{ij},K_{ij},\alpha,\beta^i\right\}$ and their derivatives in terms of the BSSN quantities $\left\{\bar{\gamma}_{ij},\text{cf},\bar{A}_{ij},\text{tr}K,\alpha,\beta^i\right\}$ and their derivatives.

### A Note on Notation:

As is standard in NRPy+,

* Greek indices refer to four-dimensional quantities where the zeroth component indicates temporal (time) component.
* Latin indices refer to three-dimensional quantities. This is somewhat counterintuitive since Python always indexes its lists starting from 0. As a result, the zeroth component of three-dimensional quantities will necessarily indicate the first *spatial* direction.

As a corollary, any expressions in NRPy+ involving mixed Greek and Latin indices will need to offset one set of indices by one; a Latin index in a four-vector will be incremented and a Greek index in a three-vector will be decremented (however, the latter case does not occur in this tutorial module).

# Table of Contents
$$\label{toc}$$

This module is organized as follows

1. [Step 1](#initializenrpy): Initialize core Python/NRPy+ modules
1. [Step 2](#threemetric): The ADM three-metric $\gamma_{ij}$ and its derivatives in terms of rescaled BSSN quantities
    1. [Step 2.a](#derivatives_e4phi): Derivatives of $e^{4\phi}$
    1. [Step 2.b](#derivatives_adm_3metric): Derivatives of the ADM three-metric: $\gamma_{ij,k}$ and $\gamma_{ij,kl}$
    1. [Step 2.c](#christoffel): Christoffel symbols  $\Gamma^i_{jk}$  associated with the ADM 3-metric $\gamma_{ij}$
1. [Step 3](#extrinsiccurvature): The ADM extrinsic curvature $K_{ij}$ and its derivatives in terms of rescaled BSSN quantities
1. [Step 4](#code_validation): Code Validation against `BSSN.ADM_in_terms_of_BSSN` NRPy+ module
1. [Step 5](#latex_pdf_output): Output this module to $\LaTeX$-formatted PDF file

# Step 1: Initialize core Python/NRPy+ modules \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Let's start by importing all the needed modules from Python/NRPy+:

```python
# Step 1.a: Import all needed modules from NRPy+
import sympy as sp
import NRPy_param_funcs as par
import indexedexp as ixp
import grid as gri
import finite_difference as fin
import reference_metric as rfm

# Step 1.b: Set the coordinate system for the numerical grid
par.set_parval_from_str("reference_metric::CoordSystem","Spherical")

# Step 1.c: Given the chosen coordinate system, set up
#           corresponding reference metric and needed
#           reference metric quantities
# The following function call sets up the reference metric
#    and related quantities, including rescaling matrices ReDD,
#    ReU, and hatted quantities.
rfm.reference_metric()

# Step 1.d: Set spatial dimension (must be 3 for BSSN, as BSSN is
#           a 3+1-dimensional decomposition of the general
#           relativistic field equations)
DIM = 3

# Step 1.e: Import all basic (unrescaled) BSSN scalars & tensors
import BSSN.BSSN_quantities as Bq
Bq.BSSN_basic_tensors()
gammabarDD = Bq.gammabarDD
cf         = Bq.cf
AbarDD     = Bq.AbarDD
trK        = Bq.trK

Bq.gammabar__inverse_and_derivs()
gammabarDD_dD  = Bq.gammabarDD_dD
gammabarDD_dDD = Bq.gammabarDD_dDD

Bq.AbarUU_AbarUD_trAbar_AbarDD_dD()
AbarDD_dD = Bq.AbarDD_dD
```

# Step 2: The ADM three-metric $\gamma_{ij}$ and its derivatives in terms of rescaled BSSN quantities. \[Back to [top](#toc)\]
$$\label{threemetric}$$

The ADM three-metric is written in terms of the covariant BSSN three-metric tensor as (Eqs. 2 and 3 of [Ruchlin *et al.*](https://arxiv.org/pdf/1712.07658.pdf)):
$$
\gamma_{ij} = \left(\frac{\gamma}{\bar{\gamma}}\right)^{1/3} \bar{\gamma}_{i j},
$$
where $\gamma=\det{\gamma_{ij}}$ and $\bar{\gamma}=\det{\bar{\gamma}_{ij}}$.

The "standard" BSSN conformal factor $\phi$ is given by (Eq. 3 of [Ruchlin *et al.*](https://arxiv.org/pdf/1712.07658.pdf)):

\begin{align}
\phi &= \frac{1}{12} \log\left(\frac{\gamma}{\bar{\gamma}}\right) \\
\implies e^{\phi} &= \left(\frac{\gamma}{\bar{\gamma}}\right)^{1/12} \\
\implies e^{4 \phi} &= \left(\frac{\gamma}{\bar{\gamma}}\right)^{1/3}
\end{align}

Thus the ADM three-metric may be written in terms of the BSSN three-metric and conformal factor $\phi$ as

$$
\gamma_{ij} = e^{4 \phi} \bar{\gamma}_{i j}.
$$

NRPy+'s implementation of BSSN allows for $\phi$ and two other alternative conformal factors to be defined:

\begin{align}
\chi &= e^{-4\phi} \\
W &= e^{-2\phi},
\end{align}

Thus if `"BSSN_quantities::EvolvedConformalFactor_cf"` is set to `"chi"`, then

\begin{align}
\gamma_{ij} &= \frac{1}{\chi} \bar{\gamma}_{i j} \\
&= \frac{1}{\text{cf}} \bar{\gamma}_{i j},
\end{align}

and if `"BSSN_quantities::EvolvedConformalFactor_cf"` is set to `"W"`, then
\begin{align}
\gamma_{ij} &= \frac{1}{W^2} \bar{\gamma}_{i j} \\
&= \frac{1}{\text{cf}^2} \bar{\gamma}_{i j}.
\end{align}

```python
# Step 2: The ADM three-metric gammaDD and its
#         derivatives in terms of BSSN quantities.
gammaDD = ixp.zerorank2()

exp4phi = sp.sympify(0)
if par.parval_from_str("EvolvedConformalFactor_cf") == "phi":
    exp4phi    = sp.exp(4*cf)
elif par.parval_from_str("EvolvedConformalFactor_cf") == "chi":
    exp4phi    = (1 / cf)
elif par.parval_from_str("EvolvedConformalFactor_cf") == "W":
    exp4phi    = (1 / cf**2)
else:
    print("Error EvolvedConformalFactor_cf type = \""+par.parval_from_str("EvolvedConformalFactor_cf")+"\" unknown.")
    exit(1)

for i in range(DIM):
    for j in range(DIM):
        gammaDD[i][j] = exp4phi*gammabarDD[i][j]
```

## Step 2.a: Derivatives of $e^{4\phi}$ \[Back to [top](#toc)\]
$$\label{derivatives_e4phi}$$

To compute derivatives of $\gamma_{ij}$ in terms of BSSN variables and their derivatives, we will first need derivatives of $e^{4\phi}$ in terms of the conformal BSSN variable `cf`.

\begin{align}
\frac{\partial}{\partial x^i} e^{4\phi} &= 4 e^{4\phi} \phi_{,i} \\
\implies \frac{\partial}{\partial x^j} \frac{\partial}{\partial x^i} e^{4\phi} &= \frac{\partial}{\partial x^j} \left(4 e^{4\phi} \phi_{,i}\right) \\
&= 16 e^{4\phi} \phi_{,i} \phi_{,j} + 4 e^{4\phi}  \phi_{,ij}
\end{align}

Thus computing first and second derivatives of $e^{4\phi}$ in terms of the BSSN quantity  `cf` requires only that we evaluate $\phi_{,i}$ and $\phi_{,ij}$ in terms of $e^{4\phi}$ (computed above in terms of `cf`) and derivatives of `cf`:

If `"BSSN_quantities::EvolvedConformalFactor_cf"` is set to `"phi"`, then
\begin{align}
\phi_{,i} &= \text{cf}_{,i} \\
\phi_{,ij} &= \text{cf}_{,ij}
\end{align}

If `"BSSN_quantities::EvolvedConformalFactor_cf"` is set to `"chi"`, then
\begin{align}
\text{cf} = e^{-4\phi} \implies \text{cf}_{,i} &= -4 e^{-4\phi} \phi_{,i} \\
\implies \phi_{,i} &= -\frac{e^{4\phi}}{4} \text{cf}_{,i} \\
\implies \phi_{,ij} &= -e^{4\phi} \phi_{,j} \text{cf}_{,i} -\frac{e^{4\phi}}{4} \text{cf}_{,ij}\\
&= -e^{4\phi} \left(-\frac{e^{4\phi}}{4} \text{cf}_{,j}\right) \text{cf}_{,i} -\frac{e^{4\phi}}{4} \text{cf}_{,ij} \\
&= \frac{1}{4} \left[\left(e^{4\phi}\right)^2 \text{cf}_{,i} \text{cf}_{,j} -e^{4\phi} \text{cf}_{,ij}\right] \\
\end{align}

If `"BSSN_quantities::EvolvedConformalFactor_cf"` is set to `"W"`, then
\begin{align}
\text{cf} = e^{-2\phi} \implies \text{cf}_{,i} &= -2 e^{-2\phi} \phi_{,i} \\
\implies \phi_{,i} &= -\frac{e^{2\phi}}{2} \text{cf}_{,i} \\
\implies \phi_{,ij} &= -e^{2\phi} \phi_{,j} \text{cf}_{,i} -\frac{e^{2\phi}}{2} \text{cf}_{,ij}\\
&= -e^{2\phi} \left(-\frac{e^{2\phi}}{2} \text{cf}_{,j}\right) \text{cf}_{,i} -\frac{e^{2\phi}}{2} \text{cf}_{,ij} \\
&= \frac{1}{2} \left[e^{4\phi} \text{cf}_{,i} \text{cf}_{,j} -e^{2\phi} \text{cf}_{,ij}\right] \\
\end{align}

```python
# Step 2.a: Derivatives of $e^{4\phi}$
phidD = ixp.zerorank1()
phidDD = ixp.zerorank2()
cf_dD  = ixp.declarerank1("cf_dD")
cf_dDD = ixp.declarerank2("cf_dDD","sym01")
if par.parval_from_str("EvolvedConformalFactor_cf") == "phi":
    for i in range(DIM):
        phidD[i]  = cf_dD[i]
        for j in range(DIM):
            phidDD[i][j] = cf_dDD[i][j]
elif par.parval_from_str("EvolvedConformalFactor_cf") == "chi":
    for i in range(DIM):
        phidD[i]  = -sp.Rational(1,4)*exp4phi*cf_dD[i]
        for j in range(DIM):
            phidDD[i][j] = sp.Rational(1,4)*( exp4phi**2*cf_dD[i]*cf_dD[j] - exp4phi*cf_dDD[i][j] )
elif par.parval_from_str("EvolvedConformalFactor_cf") == "W":
    exp2phi = (1 / cf)
    for i in range(DIM):
        phidD[i]  = -sp.Rational(1,2)*exp2phi*cf_dD[i]
        for j in range(DIM):
            phidDD[i][j] = sp.Rational(1,2)*( exp4phi*cf_dD[i]*cf_dD[j] - exp2phi*cf_dDD[i][j] )
else:
    print("Error EvolvedConformalFactor_cf type = \""+par.parval_from_str("EvolvedConformalFactor_cf")+"\" unknown.")
    exit(1)

exp4phidD  = ixp.zerorank1()
exp4phidDD = ixp.zerorank2()
for i in range(DIM):
    exp4phidD[i] = 4*exp4phi*phidD[i]
    for j in range(DIM):
        exp4phidDD[i][j] = 16*exp4phi*phidD[i]*phidD[j] + 4*exp4phi*phidDD[i][j]
```

## Step 2.b: Derivatives of the ADM three-metric: $\gamma_{ij,k}$ and $\gamma_{ij,kl}$ \[Back to [top](#toc)\]
$$\label{derivatives_adm_3metric}$$

Recall the relation between the ADM three-metric $\gamma_{ij}$, the BSSN conformal three-metric $\bar{\gamma}_{i j}$, and the BSSN conformal factor $\phi$:

$$
\gamma_{ij} = e^{4 \phi} \bar{\gamma}_{i j}.
$$

Now that we have constructed derivatives of $e^{4 \phi}$ in terms of the chosen BSSN conformal factor `cf`, and the [BSSN.BSSN_quantities module](../edit/BSSN/BSSN_quantities.py) ([**tutorial**](Tutorial-BSSN_quantities.ipynb)) defines derivatives of $\bar{\gamma}_{ij}$ in terms of rescaled BSSN variables, derivatives of $\gamma_{ij}$ can be immediately constructed using the product rule:

\begin{align}
\gamma_{ij,k} &= \left(e^{4 \phi}\right)_{,k} \bar{\gamma}_{i j} + e^{4 \phi} \bar{\gamma}_{ij,k} \\
\gamma_{ij,kl} &= \left(e^{4 \phi}\right)_{,kl} \bar{\gamma}_{i j} + \left(e^{4 \phi}\right)_{,k} \bar{\gamma}_{i j,l} + \left(e^{4 \phi}\right)_{,l} \bar{\gamma}_{ij,k} + e^{4 \phi} \bar{\gamma}_{ij,kl}
\end{align}

```python
# Step 2.b: Derivatives of gammaDD, the ADM three-metric
gammaDDdD  = ixp.zerorank3()
gammaDDdDD = ixp.zerorank4()

for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            gammaDDdD[i][j][k] = exp4phidD[k]*gammabarDD[i][j] + exp4phi*gammabarDD_dD[i][j][k]
            for l in range(DIM):
                gammaDDdDD[i][j][k][l] = exp4phidDD[k][l]*gammabarDD[i][j] + \
                                         exp4phidD[k]*gammabarDD_dD[i][j][l] + \
                                         exp4phidD[l]*gammabarDD_dD[i][j][k] + \
                                         exp4phi*gammabarDD_dDD[i][j][k][l]
```

## Step 2.c: Christoffel symbols  $\Gamma^i_{jk}$  associated with the ADM 3-metric $\gamma_{ij}$ \[Back to [top](#toc)\]
$$\label{christoffel}$$

The 3-metric analog to the definition of Christoffel symbol (Eq. 1.18) in Baumgarte & Shapiro's *Numerical Relativity* is given by
$$
\Gamma^i_{jk} = \frac{1}{2} \gamma^{il} \left(\gamma_{lj,k} + \gamma_{lk,j} - \gamma_{jk,l} \right),
$$
which we implement here:

```python
# Step 2.c: 3-Christoffel symbols associated with ADM 3-metric gammaDD
# Step 2.c.i: First compute the inverse 3-metric gammaUU:
gammaUU, detgamma = ixp.symm_matrix_inverter3x3(gammaDD)

GammaUDD = ixp.zerorank3()

for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            for l in range(DIM):
                GammaUDD[i][j][k] += sp.Rational(1,2)*gammaUU[i][l]* \
                                (gammaDDdD[l][j][k] + gammaDDdD[l][k][j] - gammaDDdD[j][k][l])
```

# Step 3: The ADM extrinsic curvature $K_{ij}$ and its derivatives in terms of rescaled BSSN quantities. \[Back to [top](#toc)\]
$$\label{extrinsiccurvature}$$

The ADM extrinsic curvature may be written in terms of the BSSN trace-free extrinsic curvature tensor $\bar{A}_{ij}$ and the trace of the ADM extrinsic curvature $K$:

\begin{align}
K_{ij} &= \left(\frac{\gamma}{\bar{\gamma}}\right)^{1/3} \bar{A}_{ij} + \frac{1}{3} \gamma_{ij} K \\
&= e^{4\phi} \bar{A}_{ij} + \frac{1}{3} \gamma_{ij} K \\
\end{align}

We only compute first spatial derivatives of $K_{ij}$, as higher-derivatives are generally not needed:
$$
K_{ij,k} = \left(e^{4\phi}\right)_{,k} \bar{A}_{ij} + e^{4\phi} \bar{A}_{ij,k} + \frac{1}{3} \left(\gamma_{ij,k} K + \gamma_{ij} K_{,k}\right)
$$
which is expressed in terms of quantities already defined.

```python
# Step 3: Define ADM extrinsic curvature KDD and
#         its first spatial derivatives KDDdD
#         in terms of BSSN quantities
KDD = ixp.zerorank2()

for i in range(DIM):
    for j in range(DIM):
        KDD[i][j] = exp4phi*AbarDD[i][j] + sp.Rational(1,3)*gammaDD[i][j]*trK

KDDdD = ixp.zerorank3()
trK_dD = ixp.declarerank1("trK_dD")
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            KDDdD[i][j][k] = exp4phidD[k]*AbarDD[i][j] + exp4phi*AbarDD_dD[i][j][k] + \
                             sp.Rational(1,3)*(gammaDDdD[i][j][k]*trK + gammaDD[i][j]*trK_dD[k])
```

# Step 4: Code Validation against `BSSN.ADM_in_terms_of_BSSN` NRPy+ module \[Back to [top](#toc)\]
$$\label{code_validation}$$

Here, as a code validation check, we verify agreement in the SymPy expressions between

1. this tutorial and
2. the NRPy+ [BSSN.ADM_in_terms_of_BSSN](../edit/BSSN/ADM_in_terms_of_BSSN.py) module.

```python
all_passed=True
def comp_func(expr1,expr2,basename,prefixname2="Bq."):
    if str(expr1-expr2)!="0":
        print(basename+" - "+prefixname2+basename+" = "+ str(expr1-expr2))
        all_passed=False

def gfnm(basename,idx1,idx2=None,idx3=None,idx4=None):
    if idx2==None:
        return basename+"["+str(idx1)+"]"
    if idx3==None:
        return basename+"["+str(idx1)+"]["+str(idx2)+"]"
    if idx4==None:
        return basename+"["+str(idx1)+"]["+str(idx2)+"]["+str(idx3)+"]"
    return basename+"["+str(idx1)+"]["+str(idx2)+"]["+str(idx3)+"]["+str(idx4)+"]"

expr_list = []
exprcheck_list = []
namecheck_list = []

import BSSN.ADM_in_terms_of_BSSN as AB
AB.ADM_in_terms_of_BSSN()

namecheck_list.extend(["detgamma"])
exprcheck_list.extend([AB.detgamma])
expr_list.extend([detgamma])
for i in range(DIM):
    for j in range(DIM):
        namecheck_list.extend([gfnm("gammaDD",i,j),gfnm("gammaUU",i,j),gfnm("KDD",i,j)])
        exprcheck_list.extend([AB.gammaDD[i][j],AB.gammaUU[i][j],AB.KDD[i][j]])
        expr_list.extend([gammaDD[i][j],gammaUU[i][j],KDD[i][j]])
        for k in range(DIM):
            namecheck_list.extend([gfnm("gammaDDdD",i,j,k),gfnm("GammaUDD",i,j,k),gfnm("KDDdD",i,j,k)])
            exprcheck_list.extend([AB.gammaDDdD[i][j][k],AB.GammaUDD[i][j][k],AB.KDDdD[i][j][k]])
            expr_list.extend([gammaDDdD[i][j][k],GammaUDD[i][j][k],KDDdD[i][j][k]])
            for l in range(DIM):
                namecheck_list.extend([gfnm("gammaDDdDD",i,j,k,l)])
                exprcheck_list.extend([AB.gammaDDdDD[i][j][k][l]])
                expr_list.extend([gammaDDdDD[i][j][k][l]])

for i in range(len(expr_list)):
    comp_func(expr_list[i],exprcheck_list[i],namecheck_list[i])

if all_passed:
    print("ALL TESTS PASSED!")
```

    ALL TESTS PASSED!

# Step 5: Output this module to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-ADM_in_terms_of_BSSN.pdf](Tutorial-ADM_in_terms_of_BSSN.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx Tutorial-ADM_in_terms_of_BSSN.ipynb
!pdflatex -interaction=batchmode Tutorial-ADM_in_terms_of_BSSN.tex
!pdflatex -interaction=batchmode Tutorial-ADM_in_terms_of_BSSN.tex
!pdflatex -interaction=batchmode Tutorial-ADM_in_terms_of_BSSN.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-ADM_in_terms_of_BSSN.ipynb to latex
    [NbConvertApp] Writing 60742 bytes to Tutorial-ADM_in_terms_of_BSSN.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


# Music machine learning - Neural networks

### Author: Philippe Esling (esling@ircam.fr)

In this course we will cover
1. A [quick introduction](#intro) on the code that we will adress and auxiliary functions
2. A simple implementation for a [single neuron](#single)
3. An implementation of [regression](#implem) using scikit-learn
4. Some [common good practices](#practices) in machine learning

# Introducing neural networks

In this tutorial, we will cover a more advanced classification algorithm through the use of *neural networks*. The tutorial starts by performing a simple **single neuron** discrimination of two random distributions. Then, we will study the typical **XOR problem** by using a more advanced 2-layer **perceptron**. Finally, we generalize the use of neural networks in order to perform classification on a given set of audio files.

We are going to use relatively _low-level_ libraries to perform the first exercises (implementing your own neurons)

```python
import time
import numpy as np
%matplotlib inline
from matplotlib import pyplot as plt
```

To simplify your work, we provide a first set of functions that provides simple plotting functionnalities (from the `helper_plot.py` file)

  |**File**|*Explanation*|
  |-------:|:---------|
  |`plot_boundary`|Plots the decision boundary of a single neuron with 2-dimensional inputs|
  |`plot_patterns`|Plots (bi-dimensionnal) input patterns|

```python
from helper_plot import plot_boundary
from helper_plot import plot_patterns
```

Remember that a single neuron is only able to learn _linearly separable_ problems. To produce such classes of problems, we provide a script that draw a set of random 2-dimensional points, then choose a random line in this space that will act as the linear frontier between 2 classes (hence defining a linear 2-class problem). The variables that will be used by your code are the following.

```Matlab
desired       % classes of the patterns
inputs        % 2 x n final matrix of random input patterns
weights       % 2 x 1 vector of neuron weights
bias          % 1 x 1 vector of bias
```

You can execute the code below to see our simple classification problem. (Note that running the same cell multiple times produces a different starting dataset).

```python
# Number of points to generate
nPats = 30 + np.floor(np.random.rand() * 30);
# Generate 2-dimensional random points
patterns = np.random.rand(2, int(nPats)) * 2 - 1;
# Slope of separating line
slope = np.log(np.random.rand() * 10);
yint = np.random.rand() * 2 - 1;
# Create the indexes for a two-class problem
desired = (patterns[1,:] - patterns[0,:] * slope - yint > 0) * 1;
# Plot the corresponding pattern
fig = plot_patterns(patterns,desired);
```

## Single neuron

For the first parts of the tutorial, we will perform the simplest classification model possible in a neural network setting, a single neuron. We briefly recall here that; given an input vector $ \mathbf{x} \in \mathbb{R}^{n} $, a single neuron computes the function

$$
\begin{equation}
y=\sigma\left(\sum_{i = 1}^{n}w_{i}.x_{i} + b\right)
\label{eq1}
\end{equation}
$$

with $ \mathbf{w} \in \mathbb{R}^{n} $ a weight vector, $ b $ a bias and $ \sigma\left(\right) $ an *activation function*. Therefore, if we consider the *threshold* activation function ($ \sigma_0\left(x\right)=1 $ if $ x \geq 0$), a single neuron simply performs an *affine transform* and then a *linear* discrimination of the space. A network will be composed of _layers_ of these neurons, which produce successive computations

Geometrically, a single neuron computes an hyperplane that separates the space. In order to learn, we have to adjust the weights and know "how much wrong we are". To do so, we consider that we know the desired output $ d $ of a system for a given example $ \mathbf{x} $ (eg. a predicted value for a regression system, a class value for a classification system). Therefore, we define the loss function $ \mathcal{L}_{\mathcal{D}} $ over a whole dataset as

$$
\begin{equation}
\mathcal{L}=\sum_{j=1}^{k_{\mathcal{D}}}\left\Vert d_{j}-y_{j}\right\Vert ^{2}
\label{eq2}
\end{equation}
$$

In order to know how to change the weights based on the value of the errors, we need to now "how to change it to make it better". Therefore, we should compute the sets of derivatives of the error given each parameter

$$
\begin{equation}
\Delta\bar{\mathbf{w}}=\left(\frac{\delta\mathcal{L}_{\mathcal{D}}}{\delta w_{1}},\ldots,\frac{\delta\mathcal{L}_{\mathcal{D}}}{\delta w_{n}}\right)
\label{eq3}
\end{equation}
$$

***

**Exercise**

  1. Perform the derivatives of the output given a single neuron
  2. Perform the derivatives for the bias as well

***

### Training your own neuron

We will start by training a single neuron to learn how to perform this discrimination with a linear problem (so that a single neuron is enough to solve it).

```python
# Inputs to use
inputs = patterns;
# Initialize the weights
weights = np.random.randn(1, 2);
bias = np.random.randn(1, 1)
# Learning rate
eta = 1;
# Weight decay
lambdaW = 0.1
```

Now you need to update the following code loop to ensure that your neuron learns to separate between the classes
***

**Exercise**
  1. Update the loop so that it computes the forward propagation error
  2. Update the loop to perform learning (based on back-propagation)
  3. Run the learning procedure, which should produce a result similar to that displayed on the website
  4. Perform multiple re-runs by **tweaking the hyperparameters** (learning rate, weight decay)
  5. What observations can you make on the learning process?
  6. (Optional) Change the input patterns, and confirm your observations.
  6. (Optional) Incorporate the bias in the weights to obtain a **vectorized** code.

***

```python
# Plot the corresponding pattern
fig = plot_patterns(patterns,desired);
plt.draw()
# Update loop
for i in range(50):

    ######################
    # YOUR CODE GOES HERE
    ######################

    ######################
    # Solution
    result = np.dot(weights, inputs) + bias > 0
    if np.sum(np.abs(result - desired)) == 0:
        break
    error = desired - result
    weights = weights + eta * (np.dot(error, inputs.transpose()))
    bias = bias + eta * (np.sum(error))
    ######################

    print('%2d.  weights = %f, %f, %f' % (i, bias[0, 0], weights[0, 0], weights[0, 1]))
    plot_boundary(np.concatenate((bias, weights), axis=1), i, '--', fig)
    plt.draw()
    time.sleep(0.2)
plot_boundary(np.concatenate((bias, weights), axis=1), i, '-', fig);
```

### 2-layer XOR problem

In most cases, classification problems are far from being linear. Therefore, we need more advanced methods to be able to compute non-linear class boundaries. The advantage of neural networks is that the same principle can be applied in a *layer-wise* fashion. This allows to further discriminate the space in sub-regions (as seen in the course). We will try to implement the 2-layer *perceptron* that can provide a solution to the infamous XOR problem. The idea is now to have the output of the first neurons to be connected to a set of other neurons. Therefore, if we take back our previous formulation, we have the same output for the first neuron(s) $y$, that we will now term as $y_{1}$. Then, we feed these outputs to a second layer of neurons, which gives

$$
\begin{equation}
y_{2}=\sigma\left(\sum_{i = 1}^{n}w_{i}.y_{1}^{i} + b\right)
\label{eq6}
\end{equation}
$$

Finally, we will rely on the same loss $\mathcal{L_{D}}$ as in the previous exercise, but the outputs used are $y_2$ instead of $y$. As in the previous case, we now need to compute the derivatives of the weights and biases for several layers . However, you should see that some form of generalization might be possible for any number of layer.

***

**Exercise**
  1. Perform the derivatives for the last layer specifically
  2. Define a generalized derivative for any previous layer

***

We can construct the prototypical set of XOR values by using the following code (note that this is the most simple case, but still this is typically a problem that cannot be solved by a _linear classifier_

```python
patterns = np.array([[-1, -1],[-1,  1],[1, -1],[1,  1]]).transpose() # Input patterns
desired = np.array([0, 1, 1, 0])                       # Corresponding classes
# Initialize based on their sizes
nInputs = patterns.shape[0]
nOutputs = 1
nPat = patterns.shape[1]
# First plot the patterns
fig = plot_patterns(patterns, desired);
```

The variables that will be used by your code are the following.

```Matlab
patterns          % 2 x n matrix of random points
desired           % classes of the patterns
inputs1           % 3 x n final matrix of inputs (accounting for bias)
nHiddens          % Number of hidden units
learnRate         % Learning rate parameter
momentum          % Momentum parameter (bonus)
weights1          % 1st layer weights
weights2          % 2nd layer weights
TSS_Limit         % Sum-squared error limit
```

```python
nHiddens = 2           # Number of hidden units
learnRate = 0.001       # Learning rate parameter
momentum = 0.1         # Momentum parameter
# Overall input patterns
inputs = patterns
TSS_Limit = 0.002                                           # Sum-squared error limit
# Learning rate
eta = 0.05
```

***

**Exercise**

  1. Update the forward propagation and error computation (compared to desired).
  2. Update the back-propagation part to learn the weights of both layers.
  3. Run the learning, which should produce a result similar to that displayed below.
  4. Perform multiple re-runs of the learning procedure (re-launching with different initializations)
  5. What observations can you make on the learning process?
  6. What happens if you initialize all weights to zeros?
  7. (Optional) Implement the *sparsity* constraint in your neural network.
  7. (Optional) Implement the *weight decay* constraint in your network.
  7. (Optional) Add the *momentum* to the learning procedure.

*For optional questions, please look after the first code box for more information*

***

```python
# Weights of first and second layer
weights1 = (np.random.randn(nHiddens, nInputs) * 0.01)      # 1st layer weights
weights2 = (np.random.randn(nOutputs, nHiddens) * 0.01)     # 2nd layer weights
bias1 = (np.random.randn(nHiddens, 1) * 0.01)               # 1st layer biases
bias2 = (np.random.randn(nOutputs, 1) * 0.01)               # 2nd layer biases
# First plot the patterns
fig = plot_patterns(patterns, desired);
# Iterate for a fixed number of iterations
deltaW1 = 0
deltaW2 = 0
deltaB1 = 0
deltaB2 = 0
for epoch in range(200):

    ######################
    # Solution

    # Forward propagate activations:
    netIn1 = np.dot(weights1, inputs) + bias1;
    result1 = np.tanh(netIn1);
    # Same with second layer
    netIn2 = np.dot(weights2, result1) + bias2;
    result2 = np.tanh(netIn2);
    #print(result2)
    # Compute total error
    resultError = result2 - desired
    TSS = np.sum(resultError ** 2);
    # Backward propagate errors:
    in2Error = resultError * (np.cosh(netIn2) ** (-2))
    # Backpropagate down to first layer
    result1Error = np.dot(weights2.transpose(), in2Error)
    in1Error = resultError * (np.cosh(netIn1) ** (-2))
    # Calculate the weight updates:
    dW2 = np.dot(in2Error, result1.transpose())
    dW1 = np.dot(in1Error, inputs.transpose())
    dB2 = np.sum(in2Error)
    dB1 = np.sum(in1Error)
    # Final updates
    deltaW2 = -eta * dW2 + momentum * deltaW2;
    deltaW1 = -eta * dW1 + momentum * deltaW1;
    deltaB2 = -eta * dB2 + momentum * deltaB2;
    deltaB1 = -eta * dB1 + momentum * deltaB1;
    # Perform weight change
    weights2 = weights2 + deltaW2;
    weights1 = weights1 + deltaW1;
    bias2 = bias2 + deltaB2;
    bias1 = bias1 + deltaB1;
    ######################

    ######################
    # YOUR CODE GOES HERE
    ######################

    #print('Epoch %3d:  Error = %f' % (epoch, TSS));
    #if TSS < TSS_Limit:
    #    break
    if (epoch - 1 % 20)==0:
        plot_boundary(np.concatenate((bias2, weights2), axis=1), epoch, '--', fig)

plot_boundary(np.concatenate((bias1, weights1), axis=1), epoch, '-', fig)
```

**Optional questions**

2. *Weight decay* constraint

As nothing constrains the weights in the network, we can note that usually all weights vector given a multiplicative factor might be equivalent, which can stall the learning (and lead to exploding weights). The *weight decay* allows to regularize the learning by penalizing weights with a too wide amplitude. The idea is to add this constraint as a term to the final loss (which leads to an indirect "pressure" on the learning process. Therefore, the final loss will be defined as

$$
\begin{equation}
\mathcal{L}_{final}=\mathcal{L_D} + \lambda \sum_{l} \sum_{i} \sum_{j} \left( W_{ij}^{l} \right)^{2}
\end{equation}
$$

where the parameter $\lambda$ controls the relative importance of the two terms.

3. *Momentum* in learning

Usually, in complex problems, the gradient can be very noisy and, therefore, the learning might oscillate widely. In order to reduce this problem, we can *smooth* the different gradient updates by retaining the values of the gradient at each iteration and then performing an update based on the latest gradient $\delta_{i}^{t}$ and the gradient at the previous iteration $\delta_{i}^{t-1}$. Therefore, a gradient update is applied as

$$
\begin{equation}
\delta_{final}^{t} = \delta_{i}^{t} + m.\delta_{i}^{t-1}
\end{equation}
$$

with $m$ the momentum parameter, which control the amount of gradient smoothing.

## 3-layer audio classification

Finally, we will attack a complete audio classification problem and try to perform neural network learning on a set of audio files. The data structure will be the same as the one used for parts 1 and 2. As discussed during the courses, even though a 2-layer neural network can provide non-linear boundaries, it can not perform "holes" inside those regions. In order to obtain an improved classification, we will now rely on a 3-layer neural network. The modification to the code of section 3.2 should be minimal, as the back-propagation will be similar for the new layer as one of the two others. We do not develop the math here as it is simply a re-application of the previous rules with an additional layer (which derivatives you should have generalized in the previous exercise).

However, up until now, we only performed *binary classification* problems, but this time we need to obtain a decision rule for multiple classes. Therefore, we cannot rely on simply computing the distance between desired patterns and the obtained binary value. The idea here is to rely on the *softmax regression*, by considering classes as a vector of probabilities. The desired answers will therefore be considered as a set of *probabilities*, where the desired class is $1$ and the others are $0$ (called *one-hot* representation). Then, the cost function will rely on the softmax formulation

$$
\begin{equation}
\mathcal{L_D}(\theta) = - \frac{1}{m} \left[ \sum_{i=1}^{m} \sum_{j=1}^{k} 1\left\{y^{(i)} = j\right\} \log \frac{e^{\theta_{j}^{T} x^{(i)}}}{\sum_{l=1}^{k} e^{ \theta_{l}^{T} x^{(i)} }}  \right]
\end{equation}
$$

Therefore, we compute the output of the softmax by taking

$$
\begin{equation}
p(y^{(i)} = j | x^{(i)}; \theta) = \frac{e^{\theta_{j}^{T} x^{(i)}}}{\sum_{l=1}^{k} e^{ \theta_{l}^{T} x^{(i)}} }
\end{equation}
$$

By taking derivatives, we can show that the gradient of the softmax layer is

$$
\begin{equation}
\nabla_{\theta_{j}} \mathcal{L_D}(\theta) = - \frac{1}{m} \sum_{i=1}^{m}{ \left[ x^{(i)} \left( 1\{ y^{(i)} = j\}  - p(y^{(i)} = j \mid x^{(i)}, \theta) \right) \right]}
\end{equation}
$$

### Sweet activation functions

As discussed in the course, the interest of stacking layers is that there is an _activation function_, which allows non-linear interactions between the dimensions (and avoids to only compute a single huge affine transform). Although the `sigmoid` function has been historically the most used, there has been some large developments since. Notably the `ReLU` (Rectified Linear Unit) is one of the major difference in modern networks (we will see more about that in a later course)

```python
# Function for computing the Sigmoid activation
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
def dsigmoid(a):
    return a * (1.0 - a)
# Function for computing the ReLU activation
def relu(x):
    return np.maximum(0, x)
def drelu(x):
    return 1 / (1 + np.exp(-x))
# Function for computing the Tanh activation
def tanh(x):
    return np.tanh(x);
def dtanh(x):
    return np.cosh(x) ^ -2
```

Here, we plot some simple examples of what these activation functions look like. You can try to rely on these functions in your previous training code and witness the differences in training.

```python
from helper_plot import prep_plots, finalize_plots
funcs = [('sigmoid',sigmoid,'red'), ('tanh',tanh,'orange'), ('relu',relu,'yellow')]
axes = prep_plots([funcs[0][0], funcs[1][0], funcs[2][0]], fig_size=(18,5), fig_num=1)
x_plot = np.linspace(-5,5,100)
np.maximum(0, x_plot)
for f in range(3):
    axes[f].plot(x_plot, funcs[f][1](x_plot), color=funcs[f][2], linewidth=4, label=funcs[f][0])
finalize_plots(axes, fig_title="Activation functions")
```

## Coding the whole network from scratch

You should now have all the tools necessary to apply neural networks from scratch to a more complex problem. In the following exercise, we simply removed any guideline code, and you need to code all the procedure for training a NN and **apply it to audio data**. You will use the spectral features discussed in the previous exercise as an input.

***

**Exercise**

  1. Based on the previous neural network, upgrade the code to a 3-layer neural network
  2. Implement the *softmax regression* on top of your 3-layer network
  3. Use the provided code to perform classification on a pre-defined set of features
  4. As previously, change the set of features to assess their different accuracies
  5. Evaluate the neural network accuracy for all features combinations
  6. What happens if the learning rate is too large ? What is this phenomenon ?
  7. (Optional) Perform a more advanced visualization of the learning process.

***

```python
# Iterate for a fixed number of iterations
for epoch in range(500):

    ######################
    # YOUR CODE GOES HERE
    ######################

    print('Epoch %3d:  Error = %f\n' % epoch, TSS);
    if TSS < TSS_Limit:
        break
plo_boundary(np.concatenate(bias1, weights1), epoch, '-');
```

## Using Pytorch to enjoy life

Up to now, we have been writing every operations by ourselves (in order to better understand the mathematics behind NN). However, there exists of course some simplifying libraries that provide large simplifications to this question.

One of the most powerful and complete library of this sort is `Pytorch`, which has been developed for several years (even prior to the recent boom of deep learning). `Pytorch` provides a large set of pre-coded layers, but also **computational graphs** and **autograd**, which are very powerful paradigms allowing to define complex operators and automatically taking derivatives.

### Defining our network

When building neural networks we frequently think of arranging the computation into layers, some of which have learnable parameters which will be optimized during learning. In `PyTorch`, the `nn` package provides higher-level abstractions over raw computational graphs that are useful for building neural networks. The `nn` package defines a set of `Modules`, which are roughly equivalent to neural network layers. A `Module` receives input `Tensors` and computes output `Tensors`, but may also hold internal state such as `Tensors` containing learnable parameters. The nn package also defines a set of useful loss functions that are commonly used when training neural networks.

In the following example, we use the `nn` package to show how easy it is to instantiate our previous three-layers network

```python
import torch
# Define the input dimensions
in_size = 1000
# Number of neurons in a layer
hidden_size = 100
# Output (target) dimension
output_size = 10
# Use the nn package to define our model and loss function.
model = torch.nn.Sequential(
    torch.nn.Linear(in_size, hidden_size),
    torch.nn.Sigmoid(),
    torch.nn.Linear(hidden_size, hidden_size),
    torch.nn.Tanh(),
    torch.nn.Linear(hidden_size, output_size),
    torch.nn.Softmax()
)
```

### Optimizing the network

Up to this point we have updated the weights of our models by manually performing the gradient descent algorithm (changing the parameters vectors). Although this is not a huge burden for simple optimization algorithms like stochastic gradient descent, in practice we often train neural networks using more sophisticated optimizers like AdaGrad, RMSProp or Adam (that we will see later in this course)

The `optim` package in PyTorch abstracts the idea of an optimization algorithm and provides implementations of commonly used optimization algorithms, and greatly simplfies the training loop associated with training a neural network.

For the sake of presentation we will use random inputs $\mathbf{x}$ that should be matched with random outputs $\mathbf{y}$

```python
batch_size = 64
# Create random Tensors to hold inputs and outputs
x = torch.randn(batch_size, in_size)
y = torch.randn(batch_size, output_size)
```

In the following example we optimize the model using the Adam algorithm provided by the `optim` package, based on a `MSE` loss.

```python
# Learning rate
learning_rate = 1e-4
# Loss function that we will use
loss_fn = torch.nn.MSELoss(reduction='sum')
# Optimizer to fit the weights of the network
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
for t in range(500):
    # Forward pass: compute predicted y by passing x to the model.
    y_pred = model(x)
    # Compute the loss.
    loss = loss_fn(y_pred, y)
    # Before the backward pass, zero all of the network gradients
    optimizer.zero_grad()
    # Backward pass: compute gradient of the loss with respect to parameters
    loss.backward()
    # Calling the step function to update the parameters
    optimizer.step()
    print(loss)
```

## Using Pytorch to classify audio

Now that we know the main components of `Pytorch` to define and optimize networks, your assignement is to define a complete classification problem from audio data, by relying on this toolbox

***

**Exercise**

  1. Use `Pytorch` to define a model for audio classification
  2. Import the audio features dataset and check that your model produces an output
  3. Write the optimization loop (think carefully about the _loss function_
  4. As previously, change the set of features to assess their different accuracies
  5. Think of how you could use more complex features (time series, audio, STFT) to classify your data

***

```python

######################
# YOUR CODE GOES HERE
######################

```


# Week 3 January 18-22: Building a Variational Monte Carlo program

**Morten Hjorth-Jensen  Email morten.hjorth-jensen@fys.uio.no**, Department of Physics and Center fo Computing in Science Education, University of Oslo, Oslo, Norway and Department of Physics and Astronomy and Facility for Rare Ion Beams, Michigan State University, East Lansing, Michigan, USA

Date: **Jan 21, 2021**

Copyright 1999-2021, Morten Hjorth-Jensen  Email morten.hjorth-jensen@fys.uio.no. Released under CC Attribution-NonCommercial 4.0 license

## Overview of week 3
**Topics.**

* Variational Monte Carlo methods, Metropolis Algorithm, statistics and Markov Chain theory

* How to structure the VMC code

## Setting up a VMC code

In setting up a C++ code for variational Monte Carlo calculations, we will use an excellent framework developed by a former Computational Physics student, Morten Ledum, now PhD student at the Hylleraas center for Quantum Chemistry.
The GitHub repository is at <https://github.com/mortele/variational-monte-carlo-fys4411>.

We will discuss this both in our forthcoming lectures and during our lab sessions.

## Introduction

### Structure and Aims

These notebooks serve the aim of linking traditional variational Monte
Carlo VMC calculations methods with recent progress on solving
many-particle problems using Machine Learning algorithms.

### This notebook

In this notebook the aim is to give you an introduction as well as an
understanding of the basic elements that are needed in order to
develop a professional variational Monte Carlo code. We will focus on
a simple system of two particles in an oscillator trap (or
alternatively two fermions moving in a Coulombic potential). The particles can
interact via a repulsive or an attrative force.  It is thus similar to the system described in project 1.

The advantage of these systems is that for two particles (boson or
fermions) we have analytical solutions for the eigenpairs of the
non-interacting case. Furthermore, for a two- or three-dimensional
system of two electrons moving in a harmonic oscillator trap, we have
[analytical solutions for the interacting case as well](https://iopscience.iop.org/article/10.1088/0305-4470/27/3/040/meta).

Having analytical eigenpairs is an invaluable feature that allows us
to assess the physical relevance of the trial wave functions, be
these either from a standard VMC procedure, from Boltzmann Machines or
from Shadow Wave functions.

In this notebook we start with the basics of a VMC calculation and
introduce concepts like Markov Chain Monte Carlo methods and the
Metropolis algorithm, importance sampling and Metropolis-Hastings
algorithm, resampling methods to obtain better estimates of the
statistical errors and minimization of the expectation values of the
energy and the variance. The latter is done in order to obtain the
best possible variational parameters. Furthermore it will define the
so-called **cost** function, a commonly encountered quantity in Machine
Learning algorithms. Minimizing the latter is the one which leads to
the determination of the optimal parameters in basically all Machine Learning algorithms.
This is a possible topic for project 2.

Topics like Markov Chain Monte Carlo and various resampling techniques
are also central to Machine Learning methods. Presenting them in the
context of VMC approaches leads hopefully to an easier starting point
for the understanding of these methods for those of you who will do the machine lerning variant of project 2.

## Basic Quantum Monte Carlo

We start with the variational principle.
Given a hamiltonian $H$ and a trial wave function $\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})$, the variational principle states that the expectation value of $\cal{E}[H]$, defined through

$$
\cal {E}[H] =
   \frac{\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R};\boldsymbol{\alpha})H(\boldsymbol{R})\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})}
        {\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R};\boldsymbol{\alpha})\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})},
$$

is an upper bound to the ground state energy $E_0$ of the hamiltonian $H$, that is

$$
E_0 \le {\cal E}[H].
$$

In general, the integrals involved in the calculation of various
expectation values are multi-dimensional ones. Traditional integration
methods such as Gauss-Legendre quadrature will not be adequate for say the
computation of the energy of a many-body system.

Here we have defined the vector $\boldsymbol{R} = [\boldsymbol{r}_1,\boldsymbol{r}_2,\dots,\boldsymbol{r}_n]$  as an array that contains the positions of all particles $n$ while the vector $\boldsymbol{\alpha} = [\alpha_1,\alpha_2,\dots,\alpha_m]$ contains the variational parameters of the model, $m$ in total.

The trial wave function can be expanded in the eigenstates $\Psi_i(\boldsymbol{R})$
of the hamiltonian since they form a complete set, viz.,

$$
\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})=\sum_i a_i\Psi_i(\boldsymbol{R}),
$$

and assuming that the set of eigenfunctions are normalized, one obtains

$$
\frac{\sum_{nm}a^*_ma_n \int d\boldsymbol{R}\Psi^{\ast}_m(\boldsymbol{R})H(\boldsymbol{R})\Psi_n(\boldsymbol{R})}
        {\sum_{nm}a^*_ma_n \int d\boldsymbol{R}\Psi^{\ast}_m(\boldsymbol{R})\Psi_n(\boldsymbol{R})} =\frac{\sum_{n}a^2_n E_n}
        {\sum_{n}a^2_n} \ge E_0,
$$

where we used that $H(\boldsymbol{R})\Psi_n(\boldsymbol{R})=E_n\Psi_n(\boldsymbol{R})$.
In general, the integrals involved in the calculation of various  expectation
values  are multi-dimensional ones.
The variational principle yields the lowest energy of states with a  given symmetry.

In most cases, a wave function has only small values in large parts of
configuration space, and a straightforward procedure which uses
homogenously distributed random points in configuration space
will most likely lead to poor results. This may suggest that some kind
of importance sampling combined with e.g., the Metropolis algorithm
may be  a more efficient way of obtaining the ground state energy.
The hope is then that those regions of configurations space where
the wave function assumes appreciable values are sampled more
efficiently.

The tedious part in a VMC calculation is the search for the variational
minimum. A good knowledge of the system is required in order to carry out
reasonable VMC calculations. This is not always the case,
and often VMC calculations
serve rather as the starting
point for so-called diffusion Monte Carlo calculations (DMC). Diffusion Monte Carlo  is a way of
solving exactly the many-body Schroedinger equation by means of
a stochastic procedure. A good guess on the binding energy
and its wave function is however necessary.
A carefully performed VMC calculation can aid in this context.

The basic procedure of a Variational Monte Carlo calculations consists thus of

1. Construct first a trial wave function $\psi_T(\boldsymbol{R};\boldsymbol{\alpha})$,  for a many-body system consisting of $n$ particles located at positions  $\boldsymbol{R}=(\boldsymbol{R}_1,\dots ,\boldsymbol{R}_n)$. The trial wave function depends on $\alpha$ variational parameters $\boldsymbol{\alpha}=(\alpha_1,\dots ,\alpha_M)$.

2. Then we evaluate the expectation value of the hamiltonian $H$

$$
\overline{E}[\boldsymbol{\alpha}]=\frac{\int d\boldsymbol{R}\Psi^{\ast}_{T}(\boldsymbol{R},\boldsymbol{\alpha})H(\boldsymbol{R})\Psi_{T}(\boldsymbol{R},\boldsymbol{\alpha})}
        {\int d\boldsymbol{R}\Psi^{\ast}_{T}(\boldsymbol{R},\boldsymbol{\alpha})\Psi_{T}(\boldsymbol{R},\boldsymbol{\alpha})}.
$$

1. Thereafter we vary $\boldsymbol{\alpha}$ according to some minimization algorithm and return eventually to the first step if we are not satisfied with the results.

Here we have used the notation $\overline{E}$ to label the expectation value of the energy.

### Linking with standard statistical expressions for expectation values

In order to bring in the Monte Carlo machinery, we define first a likelihood distribution, or probability density distribution (PDF). Using our ansatz for the trial wave function $\psi_T(\boldsymbol{R};\boldsymbol{\alpha})$ we define a PDF

$$
P(\boldsymbol{R})= \frac{\left|\psi_T(\boldsymbol{R};\boldsymbol{\alpha})\right|^2}{\int \left|\psi_T(\boldsymbol{R};\boldsymbol{\alpha})\right|^2d\boldsymbol{R}}.
$$

This is our model for  probability distribution function.
The approximation to the expectation value of the Hamiltonian is now

$$
\overline{E}[\boldsymbol{\alpha}] =
   \frac{\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R};\boldsymbol{\alpha})H(\boldsymbol{R})\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})}
        {\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R};\boldsymbol{\alpha})\Psi_T(\boldsymbol{R};\boldsymbol{\alpha})}.
$$

We define a new quantity

$$
E_L(\boldsymbol{R};\boldsymbol{\alpha})=\frac{1}{\psi_T(\boldsymbol{R};\boldsymbol{\alpha})}H\psi_T(\boldsymbol{R};\boldsymbol{\alpha}),
\label{eq:locale1} \tag{1}
$$

called the local energy, which, together with our trial PDF yields a new expression (and which look simlar to the the expressions for moments in statistics)

$$
\overline{E}[\boldsymbol{\alpha}]=\int P(\boldsymbol{R})E_L(\boldsymbol{R};\boldsymbol{\alpha}) d\boldsymbol{R}\approx \frac{1}{N}\sum_{i=1}^NE_L(\boldsymbol{R_i};\boldsymbol{\alpha})
\label{eq:vmc1} \tag{2}
$$

with $N$ being the number of Monte Carlo samples. The expression on the right hand side follows from Bernoulli's law of large numbers, which states that the sample mean, in the limit $N\rightarrow \infty$ approaches the true mean

The Algorithm for performing a variational Monte Carlo calculations runs as this

   * Initialisation: Fix the number of Monte Carlo steps. Choose an initial $\boldsymbol{R}$ and variational parameters $\alpha$ and calculate $\left|\psi_T^{\alpha}(\boldsymbol{R})\right|^2$.

   * Initialise the energy and the variance and start the Monte Carlo calculation.

      * Calculate  a trial position  $\boldsymbol{R}_p=\boldsymbol{R}+r*step$ where $r$ is a random variable $r \in [0,1]$.

      * Metropolis algorithm to accept or reject this move  $w = P(\boldsymbol{R}_p)/P(\boldsymbol{R})$.

      * If the step is accepted, then we set $\boldsymbol{R}=\boldsymbol{R}_p$.

      * Update averages

   * Finish and compute final averages.

Observe that the jumping in space is governed by the variable *step*. This is called brute-force sampling and is normally replaced by what is called **importance sampling**, discussed in more detail below here.

### Simple example, the hydrogen atom

The radial Schroedinger equation for the hydrogen atom can be
written as (when we have gotten rid of the first derivative term in the kinetic energy and used $rR(r)=u(r)$)

$$
-\frac{\hbar^2}{2m}\frac{d^2 u(r)}{d r^2}-
\left(\frac{ke^2}{r}-\frac{\hbar^2l(l+1)}{2mr^2}\right)u(r)=Eu(r).
$$

We will specialize to the case with $l=0$ and end up with

$$
-\frac{\hbar^2}{2m}\frac{d^2 u(r)}{d r^2}-
\left(\frac{ke^2}{r}\right)u(r)=Eu(r).
$$

Then we introduce a dimensionless variable $\rho=r/a$ where $a$ is a constant with dimension length.
Multiplying with $ma^2/\hbar^2$ we can rewrite our equations as

$$
-\frac{1}{2}\frac{d^2 u(\rho)}{d \rho^2}-
\frac{ke^2ma}{\hbar^2}\frac{u(\rho)}{\rho}-\lambda u(\rho)=0.
$$

Since $a$ is just a parameter we choose to set

$$
\frac{ke^2ma}{\hbar^2}=1,
$$

which leads to $a=\hbar^2/mke^2$, better known as the Bohr radius with value $0.053$ nm. Scaling the equations this way does not only render our numerical treatment simpler since we avoid carrying with us all physical parameters, but we obtain also a **natural** length scale. We will see this again and again. In our discussions below with a harmonic oscillator trap, the **natural** lentgh scale with be determined by the oscillator frequency, the mass of the particle and $\hbar$. We have also defined a dimensionless 'energy' $\lambda = Ema^2/\hbar^2$.
With the rescaled quantities, the ground state energy of the hydrogen atom is $1/2$.
The equation we want to solve is now defined by the Hamiltonian

$$
H=-\frac{1}{2}\frac{d^2 }{d \rho^2}-\frac{1}{\rho}.
$$

As trial wave function we peep now into the analytical solution for
the hydrogen atom and use (with $\alpha$ as a variational parameter)

$$
u_T^{\alpha}(\rho)=\alpha\rho \exp{-(\alpha\rho)}.
$$

Inserting this wave function into the expression for the
local energy $E_L$ gives

$$
E_L(\rho)=-\frac{1}{\rho}-
              \frac{\alpha}{2}\left(\alpha-\frac{2}{\rho}\right).
$$

To have analytical local energies saves us from computing numerically
the second derivative, a feature which often increases our numerical
expenditure with a factor of three or more. Integratng up the local energy (recall to bring back the PDF in the integration) gives  $\overline{E}[\boldsymbol{\alpha}]=\alpha(\alpha/2-1)$.

### Second example, the harmonic oscillator in one dimension

We present here another well-known example, the harmonic oscillator in
one dimension for one particle. This will also serve the aim of
introducing our next model, namely that of interacting electrons in a
harmonic oscillator trap.

Here as well, we do have analytical solutions and the energy of the
ground state, with $\hbar=1$, is $1/2\omega$, with $\omega$ being the
oscillator frequency. We use the following trial wave function

$$
\psi_T(x;\alpha) = \exp{-(\frac{1}{2}\alpha^2x^2)},
$$

which results in a local energy

$$
\frac{1}{2}\left(\alpha^2+x^2(1-\alpha^4)\right).
$$

We can compare our numerically calculated energies with the exact energy as function of $\alpha$

$$
\overline{E}[\alpha] = \frac{1}{4}\left(\alpha^2+\frac{1}{\alpha^2}\right).
$$

Similarly, with the above ansatz, we can also compute the exact variance which reads

$$
\sigma^2[\alpha]=\frac{1}{4}\left(1+(1-\alpha^4)^2\frac{3}{4\alpha^4}\right)-\overline{E}.
$$

Our code for computing the energy of the ground state of the harmonic oscillator follows here. We start by defining directories where we store various outputs.

```python
# Common imports
import os

# Where to save the figures and data files
PROJECT_ROOT_DIR = "Results"
FIGURE_ID = "Results/FigureFiles"
DATA_ID = "Results/VMCHarmonic"

if not os.path.exists(PROJECT_ROOT_DIR):
    os.mkdir(PROJECT_ROOT_DIR)

if not os.path.exists(FIGURE_ID):
    os.makedirs(FIGURE_ID)

if not os.path.exists(DATA_ID):
    os.makedirs(DATA_ID)

def image_path(fig_id):
    return os.path.join(FIGURE_ID, fig_id)

def data_path(dat_id):
    return os.path.join(DATA_ID, dat_id)

def save_fig(fig_id):
    plt.savefig(image_path(fig_id) + ".png", format='png')

outfile = open(data_path("VMCHarmonic.dat"),'w')
```

We proceed with the implementation of the Monte Carlo algorithm but list first the ansatz for the wave function and the expression for the local energy

```python
%matplotlib inline

# VMC for the one-dimensional harmonic oscillator
# Brute force Metropolis, no importance sampling and no energy minimization
from math import exp, sqrt
from random import random, seed
import numpy as np
import matplotlib.pyplot as plt
#from numba import jit
from decimal import *
# Trial wave function for the Harmonic oscillator in one dimension
def WaveFunction(r,alpha):
    return exp(-0.5*alpha*alpha*r*r)
# Local energy  for the Harmonic oscillator in one dimension
def LocalEnergy(r,alpha):
    return 0.5*r*r*(1-alpha**4) + 0.5*alpha*alpha

```

Note that in the Metropolis algorithm there is no need to compute the
trial wave function, mainly since we are just taking the ratio of two
exponentials. It is then from a computational point view, more
convenient to compute the argument from the ratio and then calculate
the exponential. Here we have refrained from this purely of
pedagogical reasons.

```python
# The Monte Carlo sampling with the Metropolis algo
# The jit decorator tells Numba to compile this function.
# The argument types will be inferred by Numba when the function is called.
#@jit
def MonteCarloSampling():

    NumberMCcycles= 1000000
    StepSize = 1.0
    # positions
    PositionOld = 0.0
    PositionNew = 0.0

    # seed for rng generator
    seed()
    # start variational parameter
    alpha = 0.8
    for ia in range(MaxVariations):
        alpha += .05
        AlphaValues[ia] = alpha
        energy = energy2 = 0.0
        #Initial position
        PositionOld = StepSize * (random() - .5)
        wfold = WaveFunction(PositionOld,alpha)
        #Loop over MC MCcycles
        for MCcycle in range(NumberMCcycles):
            #Trial position
            PositionNew = PositionOld + StepSize*(random() - .5)
            wfnew = WaveFunction(PositionNew,alpha)
            #Metropolis test to see whether we accept the move
            if random() <= wfnew**2 / wfold**2:
                PositionOld = PositionNew
                wfold = wfnew
            DeltaE = LocalEnergy(PositionOld,alpha)
            energy += DeltaE
            energy2 += DeltaE**2
        #We calculate mean, variance and error
        energy /= NumberMCcycles
        energy2 /= NumberMCcycles
        variance = energy2 - energy**2
        error = sqrt(variance/NumberMCcycles)
        Energies[ia] = energy
        Variances[ia] = variance
        outfile.write('%f %f %f %f \n' %(alpha,energy,variance,error))
    return Energies, AlphaValues, Variances
```

Finally, the results are presented here with the exact energies and variances as well.

```python
#Here starts the main program with variable declarations
MaxVariations = 20
Energies = np.zeros((MaxVariations))
ExactEnergies = np.zeros((MaxVariations))
ExactVariance = np.zeros((MaxVariations))
Variances = np.zeros((MaxVariations))
AlphaValues = np.zeros(MaxVariations)
(Energies, AlphaValues, Variances) = MonteCarloSampling()
outfile.close()
ExactEnergies = 0.25*(AlphaValues*AlphaValues+1.0/(AlphaValues*AlphaValues))
ExactVariance = 0.25*(1.0+((1.0-AlphaValues**4)**2)*3.0/(4*(AlphaValues**4)))-ExactEnergies*ExactEnergies

#simple subplot
plt.subplot(2, 1, 1)
plt.plot(AlphaValues, Energies, 'o-',AlphaValues, ExactEnergies,'r-')
plt.title('Energy and variance')
plt.ylabel('Dimensionless energy')
plt.subplot(2, 1, 2)
plt.plot(AlphaValues, Variances, '.-',AlphaValues, ExactVariance,'r-')
plt.xlabel(r'$\alpha$', fontsize=15)
plt.ylabel('Variance')
save_fig("VMCHarmonic")
plt.show()
#nice printout with Pandas
import pandas as pd
from pandas import DataFrame
data ={'Alpha':AlphaValues, 'Energy':Energies,'Exact Energy':ExactEnergies,'Variance':Variances,'Exact Variance':ExactVariance,}
frame = pd.DataFrame(data)
print(frame)
```

For $\alpha=1$ we have the exact eigenpairs, as can be deduced from the
table here. With $\omega=1$, the exact energy is $1/2$ a.u. with zero
variance, as it should. We see also that our computed variance follows rather well the exact variance.
Increasing the number of Monte Carlo cycles will improve our statistics (try to increase the number of Monte Carlo cycles).

The fact that the variance is exactly equal to zero when $\alpha=1$ is that
we then have the exact wave function, and the action of the hamiltionan
on the wave function

$$
H\psi = \mathrm{constant}\times \psi,
$$

yields just a constant. The integral which defines various
expectation values involving moments of the hamiltonian becomes then

$$
\langle H^n \rangle =
   \frac{\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R})H^n(\boldsymbol{R})\Psi_T(\boldsymbol{R})}
        {\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R})\Psi_T(\boldsymbol{R})}=
\mathrm{constant}\times\frac{\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R})\Psi_T(\boldsymbol{R})}
        {\int d\boldsymbol{R}\Psi^{\ast}_T(\boldsymbol{R})\Psi_T(\boldsymbol{R})}=\mathrm{constant}.
$$

**This gives an important information: the exact wave function leads to zero variance!**
As we will see below, many practitioners perform a minimization on both the energy and the variance.

## The Metropolis algorithm

Till now we have not yet discussed the derivation of the Metropolis algorithm. We assume the reader has some familiarity with the mathematics of Markov chains.

The Metropolis algorithm , see [the original article](http://scitation.aip.org/content/aip/journal/jcp/21/6/10.1063/1.1699114), was invented by Metropolis et. al
and is often simply called the Metropolis algorithm.
It is a method to sample a normalized probability
distribution by a stochastic process. We define $\mathbf{P}_i^{(n)}$ to
be the probability for finding the system in the state $i$ at step $n$.
The algorithm is then

* Sample a possible new state $j$ with some probability $T_{i\rightarrow j}$.

* Accept the new state $j$ with probability $A_{i \rightarrow j}$ and use it as the next sample. With probability $1-A_{i\rightarrow j}$ the move is rejected and the original state $i$ is used again as a sample.

We wish to derive the required properties of $T$ and $A$ such that
$\mathbf{P}_i^{(n\rightarrow \infty)} \rightarrow p_i$ so that starting
from any distribution, the method converges to the correct distribution.
Note that the description here is for a discrete probability distribution.
Replacing probabilities $p_i$ with expressions like $p(x_i)dx_i$ will
take all of these over to the corresponding continuum expressions.

The dynamical equation for $\mathbf{P}_i^{(n)}$ can be written directly from
the description above. The probability of being in the state $i$ at step $n$
is given by the probability of being in any state $j$ at the previous step,
and making an accepted transition to $i$ added to the probability of
being in the state $i$, making a transition to any state $j$ and
rejecting the move:

$$
\mathbf{P}^{(n)}_i = \sum_j \left [
\mathbf{P}^{(n-1)}_jT_{j\rightarrow i} A_{j\rightarrow i}
+\mathbf{P}^{(n-1)}_iT_{i\rightarrow j}\left ( 1- A_{i\rightarrow j} \right)
\right ] \,.
$$

Since the probability of making some transition must be 1,
$\sum_j T_{i\rightarrow j} = 1$, and the above equation becomes

$$
\mathbf{P}^{(n)}_i = \mathbf{P}^{(n-1)}_i +
 \sum_j \left [
\mathbf{P}^{(n-1)}_jT_{j\rightarrow i} A_{j\rightarrow i}
-\mathbf{P}^{(n-1)}_iT_{i\rightarrow j}A_{i\rightarrow j}
\right ] \,.
$$

For large $n$ we require that $\mathbf{P}^{(n\rightarrow \infty)}_i = p_i$,
the desired probability distribution. Taking this limit, gives the
balance requirement

$$
\sum_j \left [
p_jT_{j\rightarrow i} A_{j\rightarrow i}
-p_iT_{i\rightarrow j}A_{i\rightarrow j}
\right ] = 0 \,.
$$

The balance requirement is very weak. Typically the much stronger detailed
balance requirement is enforced, that is rather than the sum being
set to zero, we set each term separately to zero and use this
to determine the acceptance probabilities. Rearranging, the result is

$$
\frac{ A_{j\rightarrow i}}{A_{i\rightarrow j}}
= \frac{p_iT_{i\rightarrow j}}{ p_jT_{j\rightarrow i}} \,.
$$

The Metropolis choice is to maximize the $A$ values, that is

$$
A_{j \rightarrow i} = \min \left ( 1,
\frac{p_iT_{i\rightarrow j}}{ p_jT_{j\rightarrow i}}\right ).
$$

Other choices are possible, but they all correspond to multilplying
$A_{i\rightarrow j}$ and $A_{j\rightarrow i}$ by the same constant
smaller than unity.\footnote{The penalty function method uses just such
a factor to compensate for $p_i$ that are evaluated stochastically
and are therefore noisy.}

Having chosen the acceptance probabilities, we have guaranteed that
if the  $\mathbf{P}_i^{(n)}$ has equilibrated, that is if it is equal to $p_i$,
it will remain equilibrated. Next we need to find the circumstances for
convergence to equilibrium.

The dynamical equation can be written as

$$
\mathbf{P}^{(n)}_i = \sum_j M_{ij}\mathbf{P}^{(n-1)}_j
$$

with the matrix $M$ given by

$$
M_{ij} = \delta_{ij}\left [ 1 -\sum_k T_{i\rightarrow k} A_{i \rightarrow k}
\right ] + T_{j\rightarrow i} A_{j\rightarrow i} \,.
$$

Summing over $i$ shows that $\sum_i M_{ij} = 1$, and since
$\sum_k T_{i\rightarrow k} = 1$, and $A_{i \rightarrow k} \leq 1$, the
elements of the matrix satisfy $M_{ij} \geq 0$. The matrix $M$ is therefore
a stochastic matrix.

The Metropolis method is simply the power method for computing the
right eigenvector of $M$ with the largest magnitude eigenvalue.
By construction, the correct probability distribution is a right eigenvector
with eigenvalue 1. Therefore, for the Metropolis method to converge
to this result, we must show that $M$ has only one eigenvalue with this
magnitude, and all other eigenvalues are smaller.

## The system: two electrons in a harmonic oscillator trap in two dimensions

The Hamiltonian of the quantum dot is given by

$$
\hat{H} = \hat{H}_0 + \hat{V},
$$

where $\hat{H}_0$ is the many-body HO Hamiltonian, and $\hat{V}$ is the
inter-electron Coulomb interactions. In dimensionless units,

$$
\hat{V}= \sum_{i < j}^N \frac{1}{r_{ij}},
$$

with $r_{ij}=\sqrt{\mathbf{r}_i^2 - \mathbf{r}_j^2}$.

This leads to the  separable Hamiltonian, with the relative motion part given by ($r_{ij}=r$)

$$
\hat{H}_r=-\nabla^2_r + \frac{1}{4}\omega^2r^2+ \frac{1}{r},
$$

plus a standard Harmonic Oscillator problem  for the center-of-mass motion.
This system has analytical solutions in two and three dimensions ([M. Taut 1993 and 1994](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.48.3561)).

We want to perform  a Variational Monte Carlo calculation of the ground state of two electrons in a quantum dot well with different oscillator energies, assuming total spin $S=0$.
Our trial wave function has the following form

$$
\begin{equation}
   \psi_{T}(\boldsymbol{r}_1,\boldsymbol{r}_2) =
   C\exp{\left(-\alpha_1\omega(r_1^2+r_2^2)/2\right)}
   \exp{\left(\frac{r_{12}}{(1+\alpha_2 r_{12})}\right)},
\label{eq:trial} \tag{3}
\end{equation}
$$

where the $\alpha$s represent our variational parameters, two in this case.

Why does the trial function look like this? How did we get there?
**This will be one of our main motivations** for switching to Machine Learning later.

To find an ansatz for the correlated part of the wave function, it is
useful to rewrite the two-particle local energy in terms of the
relative and center-of-mass motion.
Let us denote the distance
between the two electrons as $r_{12}$. We omit the center-of-mass
motion since we are only interested in the case when $r_{12}
\rightarrow 0$. The contribution from the center-of-mass (CoM)
variable $\boldsymbol{R}_{\mathrm{CoM}}$ gives only a finite contribution.  We
focus only on the terms that are relevant for $r_{12}$ and for three
dimensions.

The relevant local energy becomes then

$$
\lim_{r_{12} \rightarrow 0}E_L(R)= \frac{1}{{\cal R}_T(r_{12})}\left(2\frac{d^2}{dr_{ij}^2}+\frac{4}{r_{ij}}\frac{d}{dr_{ij}}+\frac{2}{r_{ij}}-\frac{l(l+1)}{r_{ij}^2}+2E \right){\cal R}_T(r_{12})
= 0.
$$

Set $l=0$ and we have the so-called **cusp** condition

$$
\frac{d {\cal R}_T(r_{12})}{dr_{12}} = -\frac{1}{2(l+1)} {\cal R}_T(r_{12})\qquad r_{12}\to 0
$$

The above  results in

$$
{\cal R}_T  \propto \exp{(r_{ij}/2)},
$$

for anti-parallel spins and

$$
{\cal R}_T  \propto \exp{(r_{ij}/4)},
$$

for anti-parallel spins.
This is the so-called cusp condition for the relative motion, resulting in a minimal requirement
for the correlation part of the wave fuction.
For general systems containing more than say two electrons, we have this
condition for each electron pair $ij$.

### First code attempt for the two-electron case

First, as with the hydrogen case, we declare where to store files.

```python
# Common imports
import os

# Where to save the figures and data files
PROJECT_ROOT_DIR = "Results"
FIGURE_ID = "Results/FigureFiles"
DATA_ID = "Results/VMCQdotMetropolis"

if not os.path.exists(PROJECT_ROOT_DIR):
    os.mkdir(PROJECT_ROOT_DIR)

if not os.path.exists(FIGURE_ID):
    os.makedirs(FIGURE_ID)

if not os.path.exists(DATA_ID):
    os.makedirs(DATA_ID)

def image_path(fig_id):
    return os.path.join(FIGURE_ID, fig_id)

def data_path(dat_id):
    return os.path.join(DATA_ID, dat_id)

def save_fig(fig_id):
    plt.savefig(image_path(fig_id) + ".png", format='png')

outfile = open(data_path("VMCQdotMetropolis.dat"),'w')
```

Thereafter we set up the analytical expressions for the wave functions and the local energy

```python
# 2-electron VMC for quantum dot system in two dimensions
# Brute force Metropolis, no importance sampling and no energy minimization
from math import exp, sqrt
from random import random, seed
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import sys
#from numba import jit

# Trial wave function for the 2-electron quantum dot in two dims
def WaveFunction(r,alpha,beta):
    r1 = r[0,0]**2 + r[0,1]**2
    r2 = r[1,0]**2 + r[1,1]**2
    r12 = sqrt((r[0,0]-r[1,0])**2 + (r[0,1]-r[1,1])**2)
    deno = r12/(1+beta*r12)
    return exp(-0.5*alpha*(r1+r2)+deno)

# Local energy  for the 2-electron quantum dot in two dims, using analytical local energy
def LocalEnergy(r,alpha,beta):

    r1 = (r[0,0]**2 + r[0,1]**2)
    r2 = (r[1,0]**2 + r[1,1]**2)
    r12 = sqrt((r[0,0]-r[1,0])**2 + (r[0,1]-r[1,1])**2)
    deno = 1.0/(1+beta*r12)
    deno2 = deno*deno
    return 0.5*(1-alpha*alpha)*(r1 + r2) +2.0*alpha + 1.0/r12+deno2*(alpha*r12-deno2+2*beta*deno-1.0/r12)
```

The Monte Carlo sampling without importance sampling is set up here.

```python
# The Monte Carlo sampling with the Metropolis algo
# The jit decorator tells Numba to compile this function.
# The argument types will be inferred by Numba when the function is called.
#@jit
def MonteCarloSampling():

    NumberMCcycles= 10000
    StepSize = 1.0
    # positions
    PositionOld = np.zeros((NumberParticles,Dimension), np.double)
    PositionNew = np.zeros((NumberParticles,Dimension), np.double)

    # seed for rng generator
    seed()
    # start variational parameter
    alpha = 0.9
    for ia in range(MaxVariations):
        alpha += .025
        AlphaValues[ia] = alpha
        beta = 0.2
        for jb in range(MaxVariations):
            beta += .01
            BetaValues[jb] = beta
            energy = energy2 = 0.0
            DeltaE = 0.0
            #Initial position
            for i in range(NumberParticles):
                for j in range(Dimension):
                    PositionOld[i,j] = StepSize * (random() - .5)
            wfold = WaveFunction(PositionOld,alpha,beta)

            #Loop over MC MCcycles
            for MCcycle in range(NumberMCcycles):
                #Trial position moving one particle at the time
                for i in range(NumberParticles):
                    for j in range(Dimension):
                        PositionNew[i,j] = PositionOld[i,j] + StepSize * (random() - .5)
                    wfnew = WaveFunction(PositionNew,alpha,beta)

                    #Metropolis test to see whether we accept the move
                    if random() < wfnew**2 / wfold**2:
                       for j in range(Dimension):
                           PositionOld[i,j] = PositionNew[i,j]
                       wfold = wfnew
                DeltaE = LocalEnergy(PositionOld,alpha,beta)
                energy += DeltaE
                energy2 += DeltaE**2
            #We calculate mean, variance and error ...
            energy /= NumberMCcycles
            energy2 /= NumberMCcycles
            variance = energy2 - energy**2
            error = sqrt(variance/NumberMCcycles)
            Energies[ia,jb] = energy
            Variances[ia,jb] = variance
            outfile.write('%f %f %f %f %f\n' %(alpha,beta,energy,variance,error))
    return Energies, Variances, AlphaValues, BetaValues
```

And finally comes the main part with the plots as well.

```python
#Here starts the main program with variable declarations
NumberParticles = 2
Dimension = 2
MaxVariations = 10
Energies = np.zeros((MaxVariations,MaxVariations))
Variances = np.zeros((MaxVariations,MaxVariations))
AlphaValues = np.zeros(MaxVariations)
BetaValues = np.zeros(MaxVariations)
(Energies, Variances, AlphaValues, BetaValues) = MonteCarloSampling()
outfile.close()

# Prepare for plots
fig = plt.figure()
ax = fig.gca(projection='3d')
# Plot the surface.
X, Y = np.meshgrid(AlphaValues, BetaValues)
surf = ax.plot_surface(X, Y, Energies,cmap=cm.coolwarm,linewidth=0, antialiased=False)
# Customize the z axis.
zmin = np.matrix(Energies).min()
zmax = np.matrix(Energies).max()
ax.set_zlim(zmin, zmax)
ax.set_xlabel(r'$\alpha$')
ax.set_ylabel(r'$\beta$')
ax.set_zlabel(r'$\langle E \rangle$')
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.5, aspect=5)
save_fig("QdotMetropolis")
plt.show()
```

```python

```