Project 5
Contents
Project 5#
Note
There will probably be small updates to the project description in the coming days, so check back regularly!
Practicalities#
Deadline: Thursday, December 12, 23:59.
Format:
A scientific report, typeset in LaTeX, delivered as a pdf file on Canvas.
Use the report template we have provided here.
Code (with comments, of course) on a UiO GitHub repo (github.uio.no), with the URL to you repo written in the pdf document.
You must deliver via your group on Canvas (even if you are working alone).
Collaboration: We strongly encourage you to collaborate with others, in groups of up to three students. The group hands in a single pdf. Remember to list everyone’s name in the pdf.
Reproducibility: Your code should be available on a GitHub repo. Make sure to include a README file in the repo that briefly explains how the code is organised, and how it should be compiled and run in order for others to reproduce your results.
Figures: Figures included in your LaTeX document should preferrably be made as vector graphics (e.g.
.pdf
files), rather than raster graphics (e.g..png
files).
Introduction#
The goal of this project is to solve the two-dimensional time-dependent Schrödinger equation, and use it to study a double-slit setup (inside a box) and variations thereof. In short, you will write a simulation that can give results like this
. (You will not be required to animate your results, but it’s quite instructive and fun to do so.)
The key methodological aspects of the projects are:
Understanding partial differential equations (PDEs)
Applying the Crank-Nicolson method in 2+1 dimensions
Working with complex numbers in code
Getting them indices straight…
Using conserved quantities as stability checks
The Schrödinger equation#
The general formulation of the time-dependent Schrödinger equation is
where
We will now consider the case of a single, non-relativistic particle in two dimensions. If we work in “position space”, the quantum state
Here the terms
When working in position space like this, the Born rule takes the form
where
To keep things simple, we will in this project assume that all dimensionful variables have been scaled away, leaving us with a “bare” Schrödinger equation on the form
So in this equation all the variables are dimensionless, which means you do not have to worry about units for this project. Our goal is to solve equation (24) numerically to determine the evolution of the “wave function”
In our new notation the Born rule takes the form
assuming the wave function
Note
In this course we don’t expect any background knowledge in quantum mechanics, so you are not expected to discuss a lot of quantum mechanics in your report. That is, you can simply view the Schrödinger equation as a particular type of differential equation given by equation (24), where the solution is some complex-valued function
Note
For those with some quantum mechanics background: A question that sometimes come up during this project is how the wave function
Consider the one-dimensional case and for simplicity assume that we have discretised time and space (notation
and correspondingly
Since the states
Assuming that we make a position measurement at time
In this notation, the normalisation condition for the total probability can be viewed as
Notation#
Below we define the basic notation we will use for this project.
, , .We’ll use an equal step size
in both the and directions. , with . (Don’t confuse this index with the imaginary unit appearing in the Schrödinger equation!) , with . , with . . Note that the superscript here is simply a time index — we have not raised to the -th power!The matrix
is a matrix with elements . .The matrix
is a matrix with elements .
Note:
is the number of points along the axis, including the boundary points. Thus, the axis has been discretised using steps, and there are “internal points” , i.e. excluding the boundary points and .Similarly for the
axis, again with being the number of points.We will mix index notation with and without commas as needed for clarity. So keep in mind that e.g.
and mean the same thing.
Initial and boundary conditions#
Throughout this project we will assume Dirichlet boundary conditions in the
This assumption simplifies the implemention of the Crank-Nicolson scheme quite a bit, so keep in mind that the code we write will have this assumption baked in from the beginning.
For the initial wave function
Problems#
Problem 0#
Not really a problem but an advice: Before diving into this project, take a look at the Armadillo documentation to see how you can work with complex numbers. For instance, look up terms like cx_double
, cx_vec
, cx_mat
, etc.
In practice, this simply means having your Armadillo objects filled with the standard std::complex<double>
type in C++. You should read about that here and take a look at the examples.
PS: You might also find sp_cx_mat
in Armadillo very useful at some point…
Problem 1#
Show analytically that by discretising equation (24) according to the Crank-Nicolson approach, you end up with the expression
where
Problem 2#
One of the tricky things when implementing Crank-Nicolson with two spatial dimensions is getting the matrices and all the different indices right. When taking into account our choice of simple boundary conditions, equation (25) can be expressed in matrix form as
Here the vector
The parentheses are just added to make it clear where there is a change in the second index
Now we need some code to help us get this straight in our program:
Write a code snippet that translates a pair of indices
into a corresponding single index that gives the position of in the vector .Next, we need code to construct our
and matrices. The code must work for any value of , but we can use as a first test case. So write a code snippet that takes as input a number and two vectors and and produce the following matrices when the vectors are of length 9:
To get the matrix structures correct it may be useful to notice that the
and matrices are based on submatrices of size , as illustrated here:
In the Code snippets section at the bottom we provide a C++ function you can use if you want to print the structure of a sparse matrix to screen.
Make sure your code works for any value of
, even though we have so far only tested it for the simple case . One way to test that can be to check that you get the following matrices when you try with :
Now you are ready to write a proper function for your program that, using inputs
, , and the matrix as input, can fill two matrices and according to the above pattern, with(Here the
in is again the imaginary unit.)
Problem 3#
When we have the matrices
Perform the matrix multiplication
.Solve the matrix equation
for the unknown .
Given what you know about matrix
Note
If you want to implement your own solver to solve
Problem 4#
Write a part of your program that can set up the initial state
Here
Make sure that the initial state
Note
Technically, you should here also make sure to set the initial state to zero at all points inside the walls of your potential, not only on the boundary. Though, with the choice of simulation parameters we will use, where our initial state will be a wave packet concentrated “far away” from the slit walls, skipping this should not make much difference.
Also, add code that normalises your initial state such that
i.e. that the total probability in our 2D probability function
Note
By requiring that
Problem 5#
Write a part of your program that initialises the potential
Wall thickness in the
direction: 0.02Wall position (centre) in the
direction: 0.5Length of the wall piece separating the two slits (the
distance between the inner edges of the two slits): 0.05Slit aperture (opening in the
direction): 0.05Ensure that the slit setup is symmetric around
. (So for the double-slit, the wall piece separating the two slits should be centered on .)
In Problem 9 you will also use single-slit and triple-slit configurations.
Note
If you want to simulate new potential configurations without having to hard-code the potentials in your program or specify a very long list of command-line arguments, you can consider using an input text file to specify the potential configuration. See the introduction to Armadillo page for an easy example of how to read a text data table with Armadillo, or take a look at the read from file page for an example using only the standard C++ library.
Problem 6#
Put everything together into a program that does (at least) the following:
Set the simulation parameters. It may be useful to read some or all of these as command-line input, or from an input file. The main simulation parameters are
, , , , , , , , and .Set up the potential matrix,
.Set up the initial state matrix,
.Set up the matrices
and required by the Crank-Nicolson approach.Run the loop over time steps and store each new state
. You can either write every new state directly to file during the loop, or store them in memory and write everything to file after the loop. (Armadillo’scx_cube
might be useful.)
Problem 7#
Note
Note that for this problem the output file can become large-ish (~200MB as binary file) if you save the full simulation, i.e. the full wave function at each time step.
In theory, the total probability (
First run your simulation with the settings
, , , , , , , , and , i.e. without any double-slit barrier.Make a plot of the deviation of the total probability from 1.0 as a function of time. (If the deviations are too small to be visible in your plot, consider plotting the data in a different way…)
Run the simulation again, but now with a double-slit barrier switched on. Use
and the double-slit configuration from Problem 5, and make the initial state broader in the -direction by setting .Make a similar plot of the deviation of the total probability from 1.0 as a function of time.
Note
Keep in mind that how accurately you should expect the probability to be conserved will depend on what type of approach you have chosen for solving the matrix equation in Problem 3.
Problem 8#
Run your simulation with the following settings:
Make three colourmap plots that illustrate the time evolution of the 2D probability function
. Use the times , and . (Feel free to make more plots if you want, but at least include these time steps.)For the same time steps, also make colourmap plots that show
and .
Note
When making the colourmap plots, it may be a good idea adjust the colour scale to the maximum of
You may also consider not using
Problem 9#
Assume that we measure the particle with a detector screen at
(spanning the entire axis) at time . Using your simulation results from Problem 8, make a plot that shows the detection probability along this screen at this time.Since we assume that we do indeed detect the particle somewhere along this line, you should normalise the one-dimensional probability function to sum to 1.0. Or in other words, you should plot
, not along .Adjust your code to also simulate single-slit and triple-slit experiments. Like for the double-slit case, use slits with aperture 0.05 and use walls of
-length 0.05 to separate each pair of slits (in the triple-slit case). For each case, make a plot of the same 1D probability function as above, i.e. .
Problem X#
If you find it useful, feel free to make some animations of your simulation and put links to these in your report! (However, don’t overdo it — your report will be more readable if the reader doesn’t have to switch between the report and watching animations all the time.)
Code snippets#
Here’s a C++ function to print the structure of a arma::sp_cx_mat
matrix to screen:
#include <armadillo>
#include <vector>
#include <string>
// A function that prints the structure of a sparse matrix to screen.
void print_sp_matrix_structure(const arma::sp_cx_mat& A)
{
using namespace std;
using namespace arma;
// Declare a C-style 2D array of strings.
string S[A.n_rows][A.n_cols];
// Initialise all the strings to " ".
for (int i =0; i < A.n_rows; i++)
{
for (int j = 0; j < A.n_cols; j++)
{
S[i][j] = " ";
}
}
// Next, we want to set the string to a dot at each non-zero element.
// To do this we use the special loop iterator from the sp_cx_mat class
// to help us loop over only the non-zero matrix elements.
sp_cx_mat::const_iterator it = A.begin();
sp_cx_mat::const_iterator it_end = A.end();
int nnz = 0;
for(it; it != it_end; ++it)
{
S[it.row()][it.col()] = "•";
nnz++;
}
// Finally, print the matrix to screen.
cout << endl;
for (int i =0; i < A.n_rows; i++)
{
cout << "| ";
for (int j = 0; j < A.n_cols; j++)
{
cout << S[i][j] << " ";
}
cout << "|\n";
}
cout << endl;
cout << "matrix size: " << A.n_rows << "x" << A.n_cols << endl;
cout << "non-zero elements: " << nnz << endl ;
cout << endl;
}
Here’s a Python example demonstrating how you can use matplotlib to animate 2D colourmap plots:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
#
# Let's generate a dummy time series for a function z(x,y,t)
#
# Set up a 2D xy grid
h = 0.005
x_points = np.arange(0, 1+h, h)
y_points = np.arange(0, 1+h, h)
x, y = np.meshgrid(x_points, y_points, sparse=True)
# Array of time points
dt = 0.005
t_points = np.arange(0, 1+dt, dt)
# A function for a Gaussian that is travelling
# in the x direction and broadening as time passes
def z(x,y,t):
v = 0.5
x_c = 0.2
sigma_x = 0.025 + 0.15 * t
return 1. / (2 * np.pi * np.sqrt(sigma_x)) * np.exp(-0.5 * (x - x_c - v * t)**2 / sigma_x**2)
# Fill z_data_list with f(x,y,t)
z_data_list = []
for t in t_points:
z_data = z(x, y, t)
z_data_list.append(z_data)
#
# Now the list z_data_list contains a series of "frames" of z(x,y,t),
# where each frame can be plotted as a 2D image using imshow. Let's
# animate it!
#
# Some settings
fontsize = 12
t_min = t_points[0]
x_min, x_max = x_points[0], x_points[-1]
y_min, y_max = y_points[0], y_points[-1]
# Create figure
fig = plt.figure()
ax = plt.gca()
# Create a colour scale normalisation according to the max z value in the first frame
norm = matplotlib.cm.colors.Normalize(vmin=0.0, vmax=np.max(z_data_list[0]))
# Plot the first frame
img = ax.imshow(z_data_list[0], extent=[x_min,x_max,y_min,y_max], cmap=plt.get_cmap("viridis"), norm=norm)
# Axis labels
plt.xlabel("x", fontsize=fontsize)
plt.ylabel("y", fontsize=fontsize)
plt.xticks(fontsize=fontsize)
plt.yticks(fontsize=fontsize)
# Add a colourbar
cbar = fig.colorbar(img, ax=ax)
cbar.set_label("z(x,y,t)", fontsize=fontsize)
cbar.ax.tick_params(labelsize=fontsize)
# Add a text element showing the time
time_txt = plt.text(0.95, 0.95, "t = {:.3e}".format(t_min), color="white",
horizontalalignment="right", verticalalignment="top", fontsize=fontsize)
# Function that takes care of updating the z data and other things for each frame
def animation(i):
# Normalize the colour scale to the current frame?
norm = matplotlib.cm.colors.Normalize(vmin=0.0, vmax=np.max(z_data_list[i]))
img.set_norm(norm)
# Update z data
img.set_data(z_data_list[i])
# Update the time label
current_time = t_min + i * dt
time_txt.set_text("t = {:.3e}".format(current_time))
return img
# Use matplotlib.animation.FuncAnimation to put it all together
anim = FuncAnimation(fig, animation, interval=1, frames=np.arange(0, len(z_data_list), 2), repeat=False, blit=0)
# Run the animation!
plt.show()
# # Save the animation
# anim.save('./animation.mp4', writer="ffmpeg", bitrate=10000, fps=15) # The fps (frames per second) sets the animation speed