\(F=0\rightarrow F=1\) MOT temperature with the OBE

This example covers single atom evolution in a 3D MOT with no gravity using the optical Bloch equations. It highlights an interesting effect of the 3D lattice that is inherent in all MOTs.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import pylcp
import lmfit
from pylcp.common import progressBar

Define the problem

Laser beams, magnetic field, and Hamiltonian.

[2]:
laser_det = 0
det = -2.5
s = 1.25
transform = True

laserBeams = pylcp.conventional3DMOTBeams(
    s=s, delta=0., beam_type=pylcp.infinitePlaneWaveBeam
)
#laserBeams.beam_vector[2:7] = [] # Delete the y,z beams
#laserBeams.num_of_beams = 2

alpha = 1e-4
magField = pylcp.quadrupoleMagneticField(alpha)

# Hamiltonian for F=0->F=1
H_g, muq_g = pylcp.hamiltonians.singleF(F=0, gF=0, muB=1)
H_e, muq_e = pylcp.hamiltonians.singleF(F=1, gF=1, muB=1)
d_q = pylcp.hamiltonians.dqij_two_bare_hyperfine(0, 1)
hamiltonian = pylcp.hamiltonian(H_g, -det*np.eye(3)+H_e, muq_g, muq_e, d_q, mass=100)

obe = pylcp.obe(laserBeams, magField, hamiltonian,
                transform_into_re_im=transform)

Calculate the equilibrium force

Let’s try looking at a single force profile along each axis, \(\hat{x}\), \(\hat{y}\), and \(\hat{z}\):

[3]:
z = np.arange(-5.01, 5.01, 0.25)

R = {}
R['x'] = [2*z/alpha, np.zeros(z.shape), np.zeros(z.shape)]
R['y'] = [np.zeros(z.shape), 2*z/alpha, np.zeros(z.shape)]
R['z'] = [np.zeros(z.shape), np.zeros(z.shape), z/alpha]

V = {
    'x':[0.0*np.ones(z.shape), np.zeros(z.shape), np.zeros(z.shape)],
    'y':[np.zeros(z.shape), 0.0*np.ones(z.shape), np.zeros(z.shape)],
    'z':[np.zeros(z.shape), np.zeros(z.shape), 0.0*np.ones(z.shape)]
}
for key in R:
    obe.generate_force_profile(
        R[key], V[key],
        name=key, deltat_tmax=2*np.pi*100, deltat_r=4/alpha,
        itermax=1000, progress_bar=True
    )
Completed in 10.03 s.
Completed in 9.11 s.
Completed in 10.50 s.

Plot it up:

[4]:
fig, ax = plt.subplots(1, 1)
for ii, key in enumerate(obe.profile):
    ax.plot(z, obe.profile[key].F[ii])

ax.set_xlabel('$x/(\mu_B B\'/\hbar \Gamma)$')
ax.set_ylabel('$f/(\hbar k \Gamma)$');
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_7_0.png

Obviously there is something going on with the \(\hat{x}\) and \(\hat{y}\) directions. The thing that is going on is interference in the optical lattice created by the 6 beams, and it is pronounced because the atom is not moving. Let’s repeat this exercise, but average over a period of the laser lattice.

Average over the lattice

Again, note the the \(x\) and \(y\) calculation is a factor of 2 larger than \(z\).

[5]:
z = np.arange(-5.01, 5.01, 0.25)

R = {}
R['x'] = [2*z/alpha, np.zeros(z.shape), np.zeros(z.shape)]
R['y'] = [np.zeros(z.shape), 2*z/alpha, np.zeros(z.shape)]
R['z'] = [np.zeros(z.shape), np.zeros(z.shape), z/alpha]

V = {
    'x':[0.0*np.ones(z.shape), np.zeros(z.shape), np.zeros(z.shape)],
    'y':[np.zeros(z.shape), 0.0*np.ones(z.shape), np.zeros(z.shape)],
    'z':[np.zeros(z.shape), np.zeros(z.shape), 0.0*np.ones(z.shape)]
}

Npts = 128
for key in R:
    progress = progressBar()
    for ii in range(Npts):
        obe.generate_force_profile(
            R[key] + 2*np.pi*(np.random.rand(3)-0.5).reshape(3,1), V[key],
            name=key + '_%d'%ii, deltat_tmax=2*np.pi*100, deltat_r=4/alpha,
            itermax=1000, progress_bar=False
        )
        progress.update((ii+1)/Npts)
Completed in 23:25.
Completed in 41:04.
Completed in 35:21.

Now take the average:

[6]:
avgF = {}
for coord_key in R:
    avgF[coord_key] = np.sum([obe.profile[key].F for key in obe.profile if coord_key in key], axis=0)/Npts

Now plot it up:

[7]:
fig, ax = plt.subplots(1, 1)
for ii, key in enumerate(R):
    ax.plot(z, avgF[key][ii])

ax.set_xlabel('$x/(\mu_B B\'/\hbar \Gamma)$')
ax.set_ylabel('$f/(\hbar k \Gamma)$');
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_14_0.png

That looks much better.

Evolution without random scattering

One can choose various initial states. Sometimes we appear to get trapped in some lattice if we choose our initial state poorly.

[22]:
# %% Now try to evolve some initial state!
obe.v0 = np.array([0., 0., 0.])
obe.r0 = np.random.randn(3)/alpha
#obe.r0 = np.array([0., 100., 0.])
#obe.r0 = np.array([0., 0., 10.])
obe.set_initial_rho_from_rateeq()
# obe.set_initial_rho_equally()

t_span = [0, 1e4]

obe.evolve_motion(t_span,
                  progress_bar=True,
                  random_recoil=False
                 );
Completed in 1:00.

Plot it up:

[23]:
fig, ax = plt.subplots(1, 2, num='Optical Molasses F=0->F1', figsize=(6.5, 2.75))
ax[0].plot(obe.sol.t/1e3, obe.sol.r[0]/1e3,
           label='rx', linewidth=0.5)
ax[0].plot(obe.sol.t/1e3, obe.sol.r[1]/1e3,
           label='ry', linewidth=0.5)
ax[0].plot(obe.sol.t/1e3, obe.sol.r[2]/1e3,
           label='rz', linewidth=0.5)
ax[0].legend(fontsize=6)
ax[0].set_xlabel('$10^3 \Gamma t$')
ax[0].set_ylabel('$10^3 kr$')

ax[1].plot(obe.sol.t/1e3, obe.sol.v[0],
           label='vx', linewidth=0.5)
ax[1].plot(obe.sol.t/1e3, obe.sol.v[1],
           label='vy', linewidth=0.5)
ax[1].plot(obe.sol.t/1e3, obe.sol.v[2],
           label='vz', linewidth=0.5)
ax[1].legend(fontsize=6)
ax[1].set_xlabel('$10^3 \Gamma t$')
ax[1].set_ylabel('$v/(\Gamma/k)$')
fig.subplots_adjust(wspace=0.25)
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_19_0.png

Evolution with random scattering

First run another test simulation:

[24]:
# %% Now try to evolve some initial state!
obe.v0 = 0.1*np.random.randn(3)
obe.r0 = 0.1*np.random.randn(3)/alpha
obe.set_initial_rho_from_rateeq()
# obe.set_initial_rho_equally()

t_span = [0, 5e4]

obe.evolve_motion(t_span,
                  progress_bar=True,
                  random_recoil=True
                 );
Completed in 5:40.

Plot it up:

[25]:
fig, ax = plt.subplots(1, 2, num='Optical Molasses F=0->F1', figsize=(6.5, 2.75))
ax[0].plot(obe.sol.t/1e3, obe.sol.r[0]/1e3,
           label='$x$', linewidth=0.5)
ax[0].plot(obe.sol.t/1e3, obe.sol.r[1]/1e3,
           label='$y$', linewidth=0.5)
ax[0].plot(obe.sol.t/1e3, obe.sol.r[2]/1e3,
           label='$z$', linewidth=0.5)
ax[0].legend(fontsize=8)
ax[0].set_xlabel('$10^3 \Gamma t$')
ax[0].set_ylabel('$10^3 kr$')

ax[1].plot(obe.sol.t/1e3, obe.sol.v[0],
           label='vx', linewidth=0.5)
ax[1].plot(obe.sol.t/1e3, obe.sol.v[1],
           label='vy', linewidth=0.5)
ax[1].plot(obe.sol.t/1e3, obe.sol.v[2],
           label='vz', linewidth=0.5)
ax[1].set_xlabel('$10^3 \Gamma t$')
ax[1].set_ylabel('$v/(\Gamma/k)$')
fig.subplots_adjust(wspace=0.25)
/Users/steve/opt/anaconda3/lib/python3.7/site-packages/IPython/core/pylabtools.py:132: UserWarning: Creating legend with loc="best" can be slow with large amounts of data.
  fig.canvas.print_figure(bytes_io, **kw)
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_23_1.png

Now run 96 atoms. Again, we parallelize using pathos:

[12]:
import pathos
if hasattr(obe, 'sol'):
    del obe.sol

tmax = 1e5
args = ([0, tmax], )
kwargs = {'t_eval':np.linspace(0, tmax, 5001),
          'random_recoil':True,
          'progress_bar':False,
          'max_scatter_probability':0.5,
          'record_force':False}

rscale = np.array([2, 2, 2])/alpha
roffset = np.array([0.0, 0.0, 0.0])
vscale = np.array([0.1, 0.1, 0.1])
voffset = np.array([0.0, 0.0, 0.0])

def generate_random_solution(x, tmax=1e5):
    # We need to generate random numbers to prevent solutions from being seeded
    # with the same random number.
    np.random.rand(256*x)
    obe.set_initial_position(rscale*np.random.randn(3) + roffset)
    obe.set_initial_velocity(vscale*np.random.randn(3) + voffset)
    obe.set_initial_rho_from_rateeq()
    obe.evolve_motion(*args, **kwargs)

    return obe.sol

Natoms = 96
chunksize = 4
sols = []
progress = progressBar()
for jj in range(int(Natoms/chunksize)):
    with pathos.pools.ProcessPool(nodes=4) as pool:
        sols += pool.map(generate_random_solution, range(chunksize))
    progress.update((jj+1)/int(Natoms/chunksize))
Completed in 4:23:31.

Here’s another potential way to parallelize. We export a file that contains all the relevant data, and then execute the script run_single_sim.py. That grabs the data from the pickled file, and executes the sim 12 times and dumps the results into pickled files.

import dill, os

if hasattr(obe, 'sol'):
    del obe.sol

tmax = 1e5
args = ([0, tmax], )
kwargs = {'t_eval':np.linspace(0, tmax, 5001),
          'random_recoil':True,
          'recoil_velocity':0.01,
          'progress_bar':True,
          'max_scatter_probability':0.5,
          'record_force':False}

rscale = np.array([2, 2, 2])/alpha
roffset = np.array([0.0, 0.0, 0.0])
vscale = np.array([0.1, 0.1, 0.1])
voffset = np.array([0.0, 0.0, 0.0])

with open('parameters.pkl', 'wb') as output:
    dill.dump(obe, output)
    dill.dump(args, output)
    dill.dump(kwargs, output)
    dill.dump((rscale, roffset, vscale, voffset), output)

This code reads the pickled files:

files = os.listdir(path='./sims/')

sols = []
for file in files:
    if file.endswith('.pkl'):
        with open('sims/'+ file, 'rb') as input:
            sols.append(dill.load(input))

print(len(sols))

No matter which way it was parallelized, let’s plot up the result:

[13]:
fig, ax = plt.subplots(3, 2, figsize=(6.25, 2*2.75))
for sol in sols:
    for ii in range(3):
        ax[ii, 0].plot(sol.t/1e3, sol.v[ii], linewidth=0.25)
        ax[ii, 1].plot(sol.t/1e3, sol.r[ii]*alpha, linewidth=0.25)

"""for ax_i in ax[:, 0]:
    ax_i.set_ylim((-0.75, 0.75))
for ax_i in ax[:, 1]:
    ax_i.set_ylim((-4., 4.))"""
for ax_i in ax[-1, :]:
    ax_i.set_xlabel('$10^3 \Gamma t$')
for jj in range(2):
    for ax_i in ax[jj, :]:
        ax_i.set_xticklabels('')
for ax_i, lbl in zip(ax[:, 0], ['x','y','z']):
    ax_i.set_ylabel('$v_' + lbl + '/(\Gamma/k)$')
for ax_i, lbl in zip(ax[:, 1], ['x','y','z']):
    ax_i.set_ylabel('$\\alpha ' + lbl + '$')

fig.subplots_adjust(left=0.1, bottom=0.08, wspace=0.22)
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_31_0.png

Reconstruct the force:

[14]:
for sol in sols:
    sol.F, sol.f_laser, sol.f_laser_q, sol.f_mag = obe.force(sol.r, sol.t, sol.rho, return_details=True)

Concatenate all the positions and velocities and forces:

[15]:
allr = np.concatenate([sol.r[:, 500:].T for sol in sols]).T
allv = np.concatenate([sol.v[:, 500:].T for sol in sols]).T
allF = np.concatenate([sol.F[:, 500:].T for sol in sols]).T

Try to simulate what an image might look like (but we have to make it far more grainy because we have far fewer atoms):

[19]:
k = np.pi/2/780E-6
img, y_edges, z_edges = np.histogram2d(allr[1, ::100]/k, allr[2, ::100]/k, bins=[np.arange(-5., 5.01, 0.15), np.arange(-5., 5.01, 0.15)])

fig, ax = plt.subplots(1, 1)
im = ax.imshow(img.T, origin='bottom',
               extent=(np.amin(y_edges), np.amax(y_edges),
                       np.amin(z_edges), np.amax(z_edges)),
               cmap='Blues',
               aspect='equal')

ax.set_xlabel('$y$ (mm)')
ax.set_ylabel('$z$ (mm)');
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_37_0.png

Let’s evaluate the temperature as a function of time:

[20]:
t_eval = kwargs['t_eval']
vs = np.nan*np.zeros((len(sols), 3, len(t_eval)))
for v, sol in zip(vs, sols):
    v[:, :sol.v.shape[1]] = sol.v

sigma_v = np.nanstd(vs, axis=0)
sigma_v.shape

fig, ax = plt.subplots(1, 1)
ax.plot(t_eval*1e-6, 2*sigma_v.T**2*hamiltonian.mass)
ax.axhline(1, color='k', linestyle='--')
ax.set_ylabel('$T/T_D$')
ax.set_xlabel('$10^6 \Gamma t$');
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_39_0.png
[21]:
# Make a bunch of bins:
xb = np.arange(-0.5, 0.5001, 0.05)
lbls = ['x', 'y', 'z']
fig, ax = plt.subplots(1, 3, figsize=(6.5, 2.75))

for ii, lbl in enumerate(lbls):
    # Make the histogram:
    ax[ii].hist(vs[:, ii, 2500::500].flatten(), bins=xb)

    # Extract the data:
    x = xb[:-1] + np.diff(xb)/2
    y = np.histogram(vs[:, ii, 2500::500].flatten(), bins=xb)[0]

    # Fit it:
    model = lmfit.models.GaussianModel()
    result = model.fit(y, x=x)

    # Plot up the fit:
    x_fit = np.linspace(-0.5, 0.5, 101)
    ax[ii].plot(x_fit, result.eval(x=x_fit))
    ax[ii].set_xlabel('$v_%s/(\Gamma/k)$'%lbl)

    # Add the temperature
    plt.text(0.03, 0.9,
             '$T_%s/T_D = %.1f$'%(lbl, 2*result.best_values['sigma']**2*hamiltonian.mass),
             transform=ax[ii].transAxes)

fig.subplots_adjust(left=0.07, wspace=0.15)
../../_images/examples_MOTs_03_F0_to_F1_3D_MOT_OBE_temperature_40_0.png