Tutorial 3: He Excitation: Initialisation with Data and Angular Shape

In this tutorial a more relaistic system will be simulated which could be the process in an experiment and the angular shape of the electron cloud will be plotted and a video of the angular shape of the electron cloud over time will be plotted. Let’s say that helium is excited to the 3D-state via electron impact from its ground state whilst a laser excites these electron-excited atoms into a high-lying 10P-state. A level diagram of the system is shown below.

HeMetastableStateExcitation

Let’s import all the libraries needed. We will need a library to read in data from a datafile. pandas is a good library to do this.

[1]:
import LASED as las
import numpy as np
import plotly.graph_objects as go
from IPython.display import Image # This is to display html images in a notebook
import pandas as pd  #This librray is used to read in the data

Setting up the System

This is a very similar system to last tutorial except for the lifetime and laser wavelength. Both of these values are taken from looking at the transition on the National Institute of Standards and Technology’s (NIST) atomic database. The page for He can be found here. The 10P state data is found here and the 3D data is found here.

To calculate the total lifetime for \(\tau_f\) and \(\tau_b\) sum the decay rates \(A_{ki}\) for each transition from the upper state and lower state to states not in the laser-excitation and then take the reciprocal of this to get the total lifetime. For example, \(\tau_f\) for the 10P state of He has many decays to lower states however the largest decay rates will contribute the most so the first three are

\[\tau_f = \frac{1}{\Gamma_{f, total}} \approx \frac{1}{\Gamma_{f,1}+\Gamma_{f,2}+\Gamma_{f,3}}\]
[2]:
tau = 80.7e3 # lifetime in ns (take from NIST 1/A_ki as this is in rad/s)
tau_f = 59.6 # lifetime of upper state to other states in ns
tau_b = 15.7 # lifetime of lower state decay to other states in ns
laser_wavelength = 900e-9 # wavelength of laser 3D to 9P state from NIST

w_e = las.angularFreq(laser_wavelength)  # Converted to angular frequency in Grad/s

# Create states for J = 2 -> J = 1
one = las.State(L = 2, S = 0, m = -2, w = 0, label = 1)
two = las.State(L = 2, S = 0, m = -1, w = 0, label = 2)
three = las.State(L = 2, S = 0, m = 0, w = 0, label = 3)
four = las.State(L = 2, S = 0, m = 1, w = 0, label = 4)
five = las.State(L = 2, S = 0, m = 2, w = 0, label = 5)
six = las.State(L = 1, S = 0, m = -1, w = w_e, label = 6)
seven = las.State(L = 1, S = 0, m = 0, w = w_e, label = 7)
eight = las.State(L = 1, S = 0, m = 1, w = w_e, label = 8)

G = [one, two, three, four, five] # ground states
E = [six, seven, eight] # excited states
Q = [0] # laser radiation polarisation

# Simulation parameters
start_time = 0
stop_time = 200 # in ns
steps = 201
time = np.linspace(start_time, stop_time, steps)

# Laser parameters
laser_intensity = 1500 # laser intensity in mW/mm^2

Next, make the LaserAtomSystem.

[3]:
helium_system = las.LaserAtomSystem(E, G, tau, Q, laser_wavelength, tau_f = tau_f,
                                 tau_b = tau_b, laser_intensity = laser_intensity)

Initialisating the Laser-Atom System with Data

Now, we need to extract the data from the datafile. This is scattering amplitude data kindly given by Igor Brey who has calculated these amplitudes from a model. The model is quite accurate for helium and gives amplitudes for various impact angles.

Igor calculated electron impact excitation amplitudes for all angles to the 3\(^1\)D\(_2\) state. With this data we can generate an initial density matrix to excite from real data. The data was generated in the collison frame.

The density matrix elements correspond to \(\rho_{M,M'} = \sum_{m'}{\langle}Mm'_f|\rho_f|M'm'_f{\rangle} = {\langle}f(M)f^*(M'){\rangle}\)

where \(f(M)\) is the scattering amplitude for state \(|J M \rangle\).

Select data from the scattering angle of 45 degrees.

[4]:
scat_data = pd.read_csv("31DAmplitudes.csv")
scat_angle_data = scat_data.loc[scat_data['Angle'] == 45]
print(scat_angle_data)
    Angle  Re a(+2)*1e-12  Im(a+2)*1e-12  Re a(+1)*1e-12  Im(a+1)*1e-12  \
45     45           8.821           19.5           14.84         -35.66

    Re a(0)*1e-12  Im(a0)*1e-12  Re(a-1)*1e-12  Im(a-1)*1e-12  Re(a-2)*1e-12  \
45          85.82        -12.27         -14.84          35.66          8.821

    Im(a-2)*1e-12
45           19.5

Now we have read in what values the amplitudes are and we can now obtain the diagonal density matrix elements. First, put the scattering amplitudes in variables and put them into a list.

[5]:
f_2 = scat_angle_data.iloc[0].at["Re a(+2)*1e-12"] + scat_angle_data.iloc[0].at["Im(a+2)*1e-12"]*1.j
f_1 = scat_angle_data.iloc[0].at["Re a(+1)*1e-12"] + scat_angle_data.iloc[0].at["Im(a+1)*1e-12"]*1.j
f_0 = scat_angle_data.iloc[0].at["Re a(0)*1e-12"] + scat_angle_data.iloc[0].at["Im(a0)*1e-12"]*1.j
f_minus1 = scat_angle_data.iloc[0].at["Re(a-1)*1e-12"] + scat_angle_data.iloc[0].at["Im(a-1)*1e-12"]*1.j
f_minus2 = scat_angle_data.iloc[0].at["Re(a-2)*1e-12"] + scat_angle_data.iloc[0].at["Im(a-2)*1e-12"]*1.j
f = [f_minus2, f_minus1, f_0, f_1, f_2]

Now, calculate the density matrix \(\rho\). Normalise the populations so that tr(𝜌) = 1.

Note: The density matrix created must be in the order with the value of \(\rho_{-m, -m}\) starting the upper left-hand side of the matrix to \(\rho_{m, m}\) at the bottom right-hand corner. \(m\) is the projection of the total angular momentum.

[6]:
rho_collision = np.zeros((len(G), len(G)), dtype = complex)

for i, population in enumerate(f):
    for j, population in enumerate(f):
        rho_collision[i][j] = f[i]*np.conj(f[j])

# Normalisation
norm = 0
for i in range(len(G)):
    norm += rho_collision[i][i]
rho_collision = rho_collision/norm
print("The J = 2 density matrix in the collision frame is \n", pd.DataFrame(np.flip(rho_collision)))
The J = 2 density matrix in the collision frame is
                     0                   1                   2  \
0  0.040126+0.000000j -0.049448+0.052905j  0.045355+0.156080j
1 -0.049448-0.052905j  0.130688+0.000000j  0.149895-0.252136j
2  0.045355-0.156080j  0.149895+0.252136j  0.658372+0.000000j
3  0.049448+0.052905j -0.130688+0.000000j -0.149895+0.252136j
4  0.040126+0.000000j -0.049448+0.052905j  0.045355+0.156080j

                    3                   4
0  0.049448-0.052905j  0.040126+0.000000j
1 -0.130688+0.000000j -0.049448-0.052905j
2 -0.149895-0.252136j  0.045355-0.156080j
3  0.130688+0.000000j  0.049448+0.052905j
4  0.049448-0.052905j  0.040126+0.000000j

Note: Above pandas is used to convert the matrix into a DataFrame just for display purposes and the display of a matrix as a DataFrame is neater and the indeces are easier to read.

Now, we must initialise the density matrix of the LaserAtomSystem with this calculated ground state density matrix. To do this, we can use the function appendDensityMatrixToRho_0(density_rho, state_type) where: * rho_0 is a column vector which is of size n\(^2\), where n is the number of energy levels in the system. This column vector represents the denisty matrix for the laser-atom system. It is flattened (one-dimensional) as it is easier to solve the equations of motion generated with this form. It is in the form of:

\[\rho_{flattened} = \begin{bmatrix} \rho_{11} \cr \rho_{12} \cr . \cr . \cr \rho_{1n} \cr \rho_{21} \cr . \cr . \cr \rho_{nn}\end{bmatrix}\]
  • density_rho is the density matrix of the ground or upper state density matrix to be input into the flattened coupled denisty matrix and then initialised as rho_0 in the LaserAtomSystem. This is of type ndarray. This is a usual 2D matrix.

  • state_type is a character of ‘e’ or ‘g’ for appending the excited state density matrix or ‘g’ for the ground state density matrix.

rho_0 is already initialised as empty with the correct size when a LaserAtomSystem is initialised.

[7]:
helium_system.appendDensityMatrixToRho_0(rho_collision, "g")
print("The initial ρ for the laser-atom system is: \n", helium_system.rho_0)
The initial ρ for the laser-atom system is:
 [[ 0.04012626+0.j        ]
 [ 0.0494475 -0.05290513j]
 [ 0.04535541+0.15607978j]
 [-0.0494475 +0.05290513j]
 [ 0.04012626+0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.0494475 +0.05290513j]
 [ 0.1306877 +0.j        ]
 [-0.1498946 +0.25213635j]
 [-0.1306877 +0.j        ]
 [ 0.0494475 +0.05290513j]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.04535541-0.15607978j]
 [-0.1498946 -0.25213635j]
 [ 0.65837208+0.j        ]
 [ 0.1498946 +0.25213635j]
 [ 0.04535541-0.15607978j]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [-0.0494475 -0.05290513j]
 [-0.1306877 +0.j        ]
 [ 0.1498946 -0.25213635j]
 [ 0.1306877 +0.j        ]
 [-0.0494475 -0.05290513j]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.04012626+0.j        ]
 [ 0.0494475 -0.05290513j]
 [ 0.04535541+0.15607978j]
 [-0.0494475 +0.05290513j]
 [ 0.04012626+0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]
 [ 0.        +0.j        ]]

Time Evolution

Model the polarisation at 30\(^{\circ}\) to the QA so rotate the system, then time evolve, and then rotate back.

[8]:
# Rotate by (90°, 30°, -90°)
theta = 30
alpha = np.pi/2
beta = theta*np.pi/180
gamma = -np.pi/2
helium_system.rotateRho_0(alpha, beta, gamma)

Time evolve:

[9]:
helium_system.timeEvolution(time)

Rotate back:

[10]:
# Rotate by (90°, -30°, -90°)
helium_system.rotateRho_t(alpha, -beta, gamma)

Plotting and Saving Data

Now, save and plot the data.

[11]:
helium_system.saveToCSV("SavedData/MetastableHeBrey1500mW.csv")
[12]:
rho_66 = [ abs(rho) for rho in helium_system.Rho_t(six, six)]
rho_77 = [abs(rho) for rho in helium_system.Rho_t(seven, seven)]
rho_88 = [abs(rho) for rho in helium_system.Rho_t(eight, eight)]

fig_upper = go.Figure(data = go.Scatter(x = time,
                                 y = rho_66,
                                 mode = 'lines',
                                    name = "Rho_66 (Upper State)",
                                    marker = dict(
                                color = 'red',
                                symbol = 'x',
                            )))

fig_upper.add_trace(go.Scatter(x = time,
                                 y = rho_77,
                                 mode = 'lines',
                                    name = "Rho_77(Upper State)",
                                    marker = dict(
                                color = 'blue',
                                symbol = 'square',
                           )))
fig_upper.add_trace(go.Scatter(x = time,
                                 y = rho_88,
                                 mode = 'lines',
                                    name = "Rho_88(Upper State)",
                                    marker = dict(
                                color = 'green',
                                symbol = 'circle',
                           )))

fig_upper.update_layout(title = "Upper Atomic Populations: He 10<sup>1</sup>P<sub>1</sub> to 3<sup>1</sup>D<sub>2</sub>, I = {:0.0f}mW, π-polarised 30° to QA".format(laser_intensity, tau/1e3, tau_f/1e3),
                 xaxis_title = "Time (ns)",
                 yaxis_title = "Population",
                font = dict(
                    size = 11))

fig_upper.write_image("SavedPlots/tutorial3-HeFigUpper.png")
Image("SavedPlots/tutorial3-HeFigUpper.png")
[12]:
../_images/tutorials_Tutorial3-MetastableHe_23_0.png
[13]:
rho11 = [ abs(rho) for rho in helium_system.Rho_t(one, one)]
rho22 = [ abs(rho) for rho in helium_system.Rho_t(two, two)]
rho33 = [ abs(rho) for rho in helium_system.Rho_t(three, three)]
rho44 = [ abs(rho) for rho in helium_system.Rho_t(four, four)]
rho55 = [ abs(rho) for rho in helium_system.Rho_t(five, five)]


fig_lower = go.Figure(data = go.Scatter(x = time,
                                 y = rho11,
                                 mode = 'lines',
                                    name = "Rho_11 (Lower State)",
                                    marker = dict(
                                color = 'red',
                                symbol = 'circle',
                            )))
fig_lower.add_trace(go.Scatter(x = time,
                                 y = rho22,
                                 mode = 'lines',
                                    name = "Rho_22 (Lower State)",
                                    marker = dict(
                                color = 'blue',
                                symbol = 'x',
                            )))
fig_lower.add_trace(go.Scatter(x = time,
                                 y = rho33,
                                 mode = 'lines',
                                    name = "Rho_33 (Lower State)",
                                    marker = dict(
                                color = 'purple',
                                symbol = 'x',
                            )))
fig_lower.add_trace(go.Scatter(x = time,
                                 y = rho44,
                                 mode = 'lines',
                                    name = "Rho_44 (Lower State)",
                                    marker = dict(
                                color = 'gold',
                                symbol = 'x',
                            )))
fig_lower.add_trace(go.Scatter(x = time,
                                 y = rho55,
                                 mode = 'lines',
                                    name = "Rho_55 (Lower State)",
                                    marker = dict(
                                color = 'green',
                                symbol = 'square',
                            )))
fig_lower.update_layout(title = "Lower Atomic Populations: He 10<sup>1</sup>P<sub>1</sub>, I = {:0.0f}mW, π-polarised 30° to QA".format(laser_intensity, tau/1e3, tau_f/1e3),
                        xaxis_title = "Time (ns)",
                        yaxis_title = "Population")

fig_lower.write_image("SavedPlots/tutorial3-HeFigLower.png")
Image("SavedPlots/tutorial3-HeFigLower.png")
[13]:
../_images/tutorials_Tutorial3-MetastableHe_24_0.png

Plotting the Angular Shape

We can plot the angular “shape” of the states \(W(\theta, \phi)\) as they evolve over time Murray 2008:

\[W(\theta, \phi) = \sum_{m_J, m_J'}\rho_{m_J, m_J'}Y_{J, m_J}(\theta, \phi)Y^*_{J,m_J'}(\theta, \phi)\]

, where \(Y_{J, m_J}(\theta, \phi)\) are the spherical harmonics for the state with total angular momentum \(J\) and projection \(m_J\).

Note: this only works for a single angular momentum state so will not produce the correct results for multiple \(F\)-states but will be correct for a single \(J\)-state.

In LASED we can plot the initial shape of the atomic state using the angularShape_0() function which takes the argument of theta and phi lists for the azimuthal (0 to 2\(\pi\)) and polar coordinates (0 to \(\pi\)) respectively as well as a state argument which equals either “g” or “e” depending on whether the angular shape of the excited or lower state in the LaserAtomSystem() will be calculated. The “angular shape” is the radius of the electron charge cloud as the azimuthal and polar coordinate are varied.

[14]:
phi = np.linspace(0, np.pi, 100)
theta = np.linspace(0, 2*np.pi, 100)
W = helium_system.angularShape_0("g", theta, phi)

The angular shape can now be plotted using the plotting package desired. Here, I use Plotly. Before plotting, a mesh grid must be made for theta and phi and these must be converted to cartesian coordinates along with the radial coordinate calculated. A helpful function is the SphericalToCartesian function which is provided by LASED which takes the meshgrid and radius as inputs and provides x,y,z coordinates to be plotted.

[16]:
phi, theta = np.meshgrid(phi, theta)  # Create a mesh grid using numpy
x, y, z = las.SphericalToCartesian(W, theta, phi)  # Create cartesian coords
fig = go.Figure(data=[go.Surface(x=x, y=y, z=z)])  # Plot the result
fig.write_image("SavedPlots/tutorial3-HeAngularShapeDStatet=0.png")
Image("SavedPlots/tutorial3-HeAngularShapeDStatet=0.png")
[16]:
../_images/tutorials_Tutorial3-MetastableHe_28_0.png

We can make a video for the evolution of the 3D-state’s electron cloud as it evolves over time in the laser-atom system. To do this, we have to generate the angular shape of the time evolution using the function angularShape_t which gives an array of angular shapes. Each angular shape must then be plotted and the images saved. Once all the images are saved they must be stitched together to create a video file. This can be done using any software package, ffmpeg is used here.

[20]:
phi = np.linspace(0, np.pi, 100)
theta = np.linspace(0, 2*np.pi, 100)
W_t = helium_system.angularShape_t("g", theta, phi)
phi, theta = np.meshgrid(phi, theta)
# Generate the images
for i, W in enumerate(W_t):
    x, y, z = las.SphericalToCartesian(W, theta, phi)
    # Plot the result
    fig = go.Figure(data=[go.Surface(x=x, y=y, z=z)])
    camera = dict(
    up=dict(x=0, y=0, z=1),
    center=dict(x=0, y=0, z=0),
    eye=dict(x=1.25, y=-1.25, z=1.5))
    fig.update_layout(scene_camera=camera)

    filenumber = "%0.5d"%i
    fig.write_image(f"Videos/3DShape" + filenumber + ".png", scale = 2.2)
[21]:
# Generate mp4 using ffpmeg
import subprocess

subprocess.call([
        'ffmpeg', '-framerate', '8', '-i', 'Videos/3DShape%05d.png', '-r', '30', '-pix_fmt', 'yuv420p',
        'He3DState1500mWCollFrame30deg.mp4'
    ])
[21]:
0