├── Examples.ipynb ├── Figure_Codes ├── Figure_10.py ├── Figure_11.py ├── Figure_12.py ├── Figure_14.py ├── Figure_15.py ├── Figure_2.py ├── Figure_3.py ├── Figure_6.py ├── Figure_7b.py ├── Figure_8.py ├── Figure_9.py ├── Figures │ ├── Fig_10.png │ ├── Fig_11.png │ ├── Fig_12.png │ ├── Fig_14.png │ ├── Fig_15.png │ ├── Fig_2.png │ ├── Fig_3.png │ ├── Fig_6.png │ ├── Fig_7b.png │ ├── Fig_8.png │ └── Fig_9.png ├── Iterative_Times.txt ├── LeastSq_Times.txt └── Matrix_Times.txt ├── Functions.ipynb ├── OBE_Tools.py ├── README.md └── requirements.txt /Figure_Codes/Figure_10.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | import OBE_Tools as OBE 4 | 5 | ## For a 3-level system, compare the results of using the numerical and 6 | ## analytical solutions for different values of Omega_12 7 | 8 | Omegas_all = numpy.asarray([[0.1,10], [5,10]]) #2pi MHz 9 | Deltas = [0,0] #2pi MHz 10 | Gammas = [5,1] #2pi MHz 11 | gammas = [0.1,0.1] #2pi MHz 12 | 13 | Delta_12s = numpy.linspace(-20,20,200) #2pi MHz 14 | absorb = numpy.zeros((2,2, len(Delta_12s))) 15 | 16 | # first consider weak-probe case, then repeat for outside weak-probe 17 | for n in range(2): 18 | Omegas = Omegas_all[n] 19 | for i, p in enumerate(Delta_12s): 20 | Deltas[0] = p 21 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 22 | probe_abs = numpy.imag(solution[1]) 23 | absorb[n,0,i] = probe_abs 24 | solution = OBE.fast_3_level(Omegas, Deltas, Gammas, gammas) 25 | probe_abs = -numpy.imag(solution) 26 | absorb[n,1,i] = probe_abs 27 | 28 | fig, [ax1, ax2] = pyplot.subplots(1,2,figsize = (7,2.5)) 29 | ax1.plot(Delta_12s, absorb[0,0,:], label = 'Matrix') 30 | ax1.plot(Delta_12s, absorb[0,1,:], label = 'Analytic', ls = 'dashed') 31 | ax1.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 32 | ax1.set_ylabel(r'Probe absorption' '\n' r'($-\Im[\rho_{21}]$)') 33 | ax1.set_xlim(numpy.min(Delta_12s), numpy.max(Delta_12s)) 34 | 35 | ax2.plot(Delta_12s, absorb[1,0,:], label = 'Matrix') 36 | ax2.plot(Delta_12s, absorb[1,1,:], label = 'Analytic', ls = 'dashed') 37 | ax2.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 38 | ax2.set_xlim(numpy.min(Delta_12s), numpy.max(Delta_12s)) 39 | 40 | pyplot.subplots_adjust( 41 | top=0.9, 42 | bottom=0.2, 43 | left=0.12, 44 | right=0.98, 45 | hspace=0.2, 46 | wspace=0.12) 47 | pyplot.show() 48 | -------------------------------------------------------------------------------- /Figure_Codes/Figure_11.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | import OBE_Tools as OBE 4 | 5 | ## For a 4-level atomic system, evaluate probe absorption as a function of 6 | ## probe detuning for varying values of Omega_34 7 | 8 | Omegas = [1,10,0] #2pi MHz 9 | Deltas = [0,0,0] #2pi MHz 10 | gammas = [0.5,0.5,0.5] #2pi MHz 11 | Gammas = [2,2,2] #2pi MHz 12 | Omega_34s = [0,5,10] #2pi MHz 13 | 14 | Delta_12s = numpy.linspace(-20,20,400) #2pi MHz 15 | probe_abs = numpy.zeros((len(Omega_34s), len(Delta_12s))) 16 | for j, O34 in enumerate(Omega_34s): 17 | Omegas[2] = O34 18 | for i, p in enumerate(Delta_12s): 19 | Deltas[0] = p 20 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 21 | probe_abs[j,i] = numpy.imag(solution[1]) 22 | 23 | pyplot.figure(figsize = (7,4)) 24 | pyplot.plot(Delta_12s, probe_abs[0,:], c = 'C1', label = r'$\Omega_{34} = 0$ MHz') 25 | pyplot.plot(Delta_12s, probe_abs[1,:], c = 'C0', ls = 'dashed', label = r'$\Omega_{34} = 5$ MHz') 26 | pyplot.plot(Delta_12s, probe_abs[2,:], c = 'C2', ls = 'dotted', label = r'$\Omega_{34} = 10$ MHz') 27 | pyplot.xlabel('Probe detuning $\Delta_{12}$ (MHz)') 28 | pyplot.ylabel(r'Probe absorption (-$\Im[\rho_{21}]$)') 29 | pyplot.legend(loc = 0) 30 | pyplot.xlim(min(Delta_12s), max(Delta_12s)) 31 | 32 | pyplot.show() -------------------------------------------------------------------------------- /Figure_Codes/Figure_12.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | import OBE_Tools as OBE 4 | 5 | ## Compare the results of the numerical and analytical solution methods for a 6 | ## 4-level system both within and beyond the weak-probe limit. 7 | 8 | Omegas_all = [[0.1,10,2],[5,10,2]] #2pi MHz 9 | Deltas = [0,0,0] #2pi MHz 10 | Gammas = [5,1,0.5] #2pi MHz 11 | gammas = [0.1,0.1,0.1] #2pi MHz 12 | 13 | Delta_12s = numpy.linspace(-20,20,200) #2pi MHz 14 | absorb = numpy.zeros((2,2, len(Delta_12s))) 15 | 16 | for n in range(2): 17 | Omegas = Omegas_all[n] 18 | for i, p in enumerate(Delta_12s): 19 | Deltas[0] = p 20 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 21 | probe_abs = numpy.imag(solution[1]) 22 | absorb[n,0,i] = probe_abs 23 | solution = OBE.fast_4_level(Omegas, Deltas, Gammas, gammas) 24 | probe_abs = -numpy.imag(solution) 25 | absorb[n,1,i] = probe_abs 26 | 27 | fig, [ax1, ax2] = pyplot.subplots(1,2,figsize = (7,2.5)) 28 | ax1.plot(Delta_12s, absorb[0,0,:], label = 'Matrix') 29 | ax1.plot(Delta_12s, absorb[0,1,:], label = 'Analytic', ls = 'dashed') 30 | ax1.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 31 | ax1.set_ylabel(r'Probe absorption' '\n' r'($-\Im[\rho_{21}]$)') 32 | ax1.set_xlim(min(Delta_12s), max(Delta_12s)) 33 | 34 | ax2.plot(Delta_12s, absorb[1,0,:], label = 'Matrix') 35 | ax2.plot(Delta_12s, absorb[1,1,:], label = 'Analytic', ls = 'dashed') 36 | ax2.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 37 | ax2.set_xlim(min(Delta_12s), max(Delta_12s)) 38 | 39 | pyplot.subplots_adjust( 40 | top=0.9, 41 | bottom=0.2, 42 | left=0.12, 43 | right=0.98, 44 | hspace=0.2, 45 | wspace=0.12) 46 | pyplot.show() 47 | -------------------------------------------------------------------------------- /Figure_Codes/Figure_14.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes 4 | import OBE_Tools as OBE 5 | 6 | ## Calculate probe absorption as a function of both probe detuning and atomic 7 | ## velocity for a 3-level system, then integrate over the Maxwell-Boltzmann 8 | ## distribution to obtain the Doppler-averaged spectrum. 9 | 10 | Omegas = [1,10] # 2pi MHz 11 | Deltas = [0,0] # 2pi MHz 12 | gammas = [0.1,0.1] # 2pi MHz 13 | Gammas = [10,1] # 2pi MHz 14 | lamdas = numpy.asarray([780, 480])*1e-9 #m 15 | dirs = numpy.asarray([1,-1]) #unitless direction vectors 16 | ks = dirs/lamdas # 2pi m^-1 17 | 18 | Delta_12s = numpy.linspace(-50,50,500) #2pi MHz 19 | velocities = numpy.linspace(-150, 150, 501) #m/s 20 | probe_abs = numpy.zeros((len(velocities), len(Delta_12s))) 21 | for i, v in enumerate(velocities): 22 | for j, p in enumerate(Delta_12s): 23 | Deltas[0] = p 24 | Deltas_eff = numpy.zeros(len(Deltas)) 25 | for k in range(len(Deltas)): 26 | Deltas_eff[k] = Deltas[k]+(ks[k]*v)*1e-6 # to convert to MHz 27 | solution = OBE.fast_3_level(Omegas, Deltas_eff, Gammas, gammas = gammas) 28 | probe_abs[i,j] = -numpy.imag(solution) 29 | 30 | # Equations of asyptotes (eienvectors) 31 | e_plus = -Delta_12s*lamdas[0]*dirs[0] 32 | e_minus = -Delta_12s/((dirs[0]/lamdas[0]) + (dirs[1]/lamdas[1])) 33 | 34 | int_manual = numpy.zeros(len(Delta_12s)) 35 | pdf = OBE.MBdist(velocities) 36 | vel_step = velocities[-1] - velocities[-2] 37 | 38 | # Perform integration 'manually' (using rectangle rule) 39 | for i in range(len(Delta_12s)): 40 | p_slice = probe_abs[:,i] 41 | weighted = p_slice*pdf*vel_step 42 | int_manual[i] = numpy.sum(weighted) 43 | 44 | fig, [ax1, ax2] = pyplot.subplots(2,1, figsize = (7,4), sharex = True) 45 | thing = ax1.imshow(probe_abs, aspect = 'auto', origin = 'lower', 46 | extent = [Delta_12s[0], Delta_12s[-1], velocities[0], velocities[-1]]) 47 | ax1.plot(Delta_12s, e_minus*1e6, c = 'w', ls = 'dotted', lw = 1, alpha = 0.8) 48 | ax1.plot(Delta_12s, e_plus*1e6, c = 'k', ls = 'dotted', lw = 1) 49 | pyplot.subplots_adjust(top=0.95, 50 | bottom=0.11, 51 | left=0.11, 52 | right=0.85, 53 | hspace=0.1, 54 | wspace=0.2) 55 | 56 | box = ax1.get_position() 57 | (x0, y0, width, height) = box.bounds 58 | gap = 0.01 59 | cax1 = pyplot.axes([x0+width+gap, y0, 0.02, height]) 60 | cbar = pyplot.colorbar(thing, cax = cax1, label = r'Probe absorption ($-\Im[\rho_{21}]$)') 61 | ax1.set_ylabel(r'Velocity (m/s)') 62 | 63 | ax2.plot(Delta_12s, int_manual) 64 | ax2.plot(Delta_12s, probe_abs[len(velocities)//2,:]) 65 | ax2.set_xlabel(r'Probe detuning $\Delta_{12}$ (MHz)') 66 | ax2.set_ylabel('Probe absorption\n' r'($-\Im[\rho_{21}]$)') 67 | 68 | in_ax = inset_axes(ax2, width = 1.5, height = 0.8, loc=1) 69 | in_ax.plot(Delta_12s, int_manual, c = 'C0') 70 | in_ax.set_xlim(-45, 45) 71 | in_ax.set_xlabel(r'$\Delta_{12}$') 72 | 73 | pyplot.show() 74 | -------------------------------------------------------------------------------- /Figure_Codes/Figure_15.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes 4 | import OBE_Tools as OBE 5 | 6 | ## Calculate probe absorption as a function of both probe detuning and atomic 7 | ## velocity for a 4-level system, then integrate over the Maxwell-Boltzmann 8 | ## distribution to obtain the Doppler-averaged spectrum. 9 | 10 | Omegas = [1,20,10] # 2pi MHz 11 | Deltas = [0,0,0] # 2pi MHz 12 | gammas = [0.1,0.1,0.1] # 2pi MHz 13 | Gammas = [10,1,1] # 2pi MHz 14 | lamdas = numpy.asarray([852, 1470, 843])*1e-9 #m 15 | dirs = numpy.asarray([1,-1, -1]) #unitless direction vectors 16 | ks = dirs/lamdas # 2pi m^-1 17 | 18 | Delta_12s = numpy.linspace(-50,50,500) #2pi MHz 19 | velocities = numpy.linspace(-150, 150, 201) #m/s 20 | probe_abs = numpy.zeros((len(velocities), len(Delta_12s))) 21 | for i, v in enumerate(velocities): 22 | for j, p in enumerate(Delta_12s): 23 | Deltas[0] = p 24 | Deltas_eff = numpy.zeros(len(Deltas)) 25 | for k in range(len(Deltas)): 26 | Deltas_eff[k] = Deltas[k]+(ks[k]*v)*1e-6 # to convert to MHz 27 | solution = OBE.fast_n_level(Omegas, Deltas_eff, Gammas, gammas = gammas) 28 | probe_abs[i,j] = -numpy.imag(solution) 29 | 30 | int_manual = numpy.zeros(len(Delta_12s)) 31 | pdf = OBE.MBdist(velocities) 32 | vel_step = velocities[-1] - velocities[-2] 33 | 34 | #Integrate 'manually' (using rectangle rule) 35 | for i in range(len(Delta_12s)): 36 | p_slice = probe_abs[:,i] 37 | weighted = p_slice*pdf*vel_step 38 | int_manual[i] = numpy.sum(weighted) 39 | 40 | colours = ['k', 'w', 'lightgray'] 41 | fig, [ax1, ax2] = pyplot.subplots(2,1, figsize = (7,4), sharex = True) 42 | thing = ax1.imshow(probe_abs, aspect = 'auto', origin = 'lower', 43 | extent = [Delta_12s[0], Delta_12s[-1], velocities[0], velocities[-1]]) 44 | for i in range(3): 45 | # Plot asymptotes 46 | ax1.plot(Delta_12s, -Delta_12s/(numpy.sum(ks[:i+1])*1e-6), ls = 'dotted', 47 | c = colours[i], lw = 1, alpha = 0.9) 48 | pyplot.subplots_adjust(top=0.95, 49 | bottom=0.11, 50 | left=0.11, 51 | right=0.85, 52 | hspace=0.1, 53 | wspace=0.2) 54 | 55 | box = ax1.get_position() 56 | (x0, y0, width, height) = box.bounds 57 | gap = 0.01 58 | cax1 = pyplot.axes([x0+width+gap, y0, 0.02, height]) 59 | cbar = pyplot.colorbar(thing, cax = cax1, label = r'Probe absorption ($-\Im[\rho_{21}]$)') 60 | ax1.set_ylabel(r'Velocity (m/s)') 61 | 62 | ax2.plot(Delta_12s, int_manual) 63 | ax2.plot(Delta_12s, probe_abs[len(velocities)//2,:]) 64 | ax2.set_xlabel(r'Probe detuning $\Delta_{12}$ (MHz)') 65 | ax2.set_ylabel('Probe absorption\n' r'($-\Im[\rho_{21}]$)') 66 | 67 | in_ax = inset_axes(ax2, width = 1.2, height = 0.95, loc=1) 68 | in_ax.plot(Delta_12s, int_manual, c = 'C0') 69 | in_ax.set_xlim(-45, 45) 70 | in_ax.set_xlabel(r'$\Delta_{12}$') 71 | 72 | pyplot.show() 73 | -------------------------------------------------------------------------------- /Figure_Codes/Figure_2.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | import OBE_Tools as OBE 4 | 5 | ## Plot the population of each state in a 2-level atomic system as a function 6 | ## of time, both with and without considering decay. 7 | 8 | 9 | times = numpy.linspace(0,8*numpy.pi,500) 10 | Omegas = [1] 11 | Deltas = [0] 12 | Gammas = [0,0.25] 13 | 14 | rho_0 = numpy.transpose(numpy.asarray([1,1,1,0])) #state vector at t=0 15 | pops = numpy.zeros((2,len(times),2)) #empty array to store populations 16 | 17 | for j,G in enumerate(Gammas): 18 | M = OBE.time_dep_matrix(Omegas, Deltas, [G]) 19 | for i, t in enumerate(times): 20 | sol = OBE.time_evolve(M, t, rho_0) # perform the time evolution 21 | #extract the populations from the solution array 22 | rho_11, rho_22 = numpy.real(sol[0]), numpy.real(sol[3]) 23 | factor = rho_11 + rho_22 24 | popn = [rho_11, rho_22] 25 | pops[j,i,:] = popn/factor # store normalised populations in array 26 | 27 | fig, [ax1, ax2] = pyplot.subplots(1,2,figsize = (7,2.5)) 28 | ax1.plot(times, numpy.real(pops[0,:,0])) 29 | ax1.plot(times, numpy.real(pops[0,:,1])) 30 | ax1.set_xlabel(r'Time ($1/\Omega$)') 31 | ax1.set_ylabel(r'Population') 32 | ax1.set_xlim(min(times), max(times)) 33 | 34 | ax2.plot(times, numpy.real(pops[1,:,0]), label = r'$|1\rangle$') 35 | ax2.plot(times, numpy.real(pops[1,:,1]), label = r'$|2\rangle$') 36 | ax2.set_xlabel(r'Time ($1/\Omega$)') 37 | ax2.set_xlim(min(times), max(times)) 38 | ax2.legend(loc=0) 39 | pyplot.subplots_adjust( 40 | top=0.9, 41 | bottom=0.2, 42 | left=0.12, 43 | right=0.98, 44 | hspace=0.2, 45 | wspace=0.12) 46 | pyplot.show() -------------------------------------------------------------------------------- /Figure_Codes/Figure_3.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes 4 | import OBE_Tools as OBE 5 | 6 | ## Evaluate the ratio of population in the ground and excited state in the 7 | ## steady state for different ratios of Gamma and Omega. 8 | ## Also look at the time exolution of the ground-state population. 9 | 10 | times = numpy.linspace(0,15*2*numpy.pi,1000) 11 | Omegas = [1] 12 | Deltas = [0] 13 | Gammas = numpy.linspace(0.05,2,20) # range of values of Gamma 14 | norm_Gammas = Gammas/numpy.max(Gammas) #used tp set line opacity in plot 15 | 16 | rho_0 = numpy.transpose(numpy.asarray([1,1,1,0])) #state vector at t=0 17 | pops = numpy.zeros((len(Gammas),len(times),2)) #empty array to store populations 18 | 19 | # For each value of Gamma, look at the time evolution of the ground state 20 | # population 21 | 22 | for j,G in enumerate(Gammas): 23 | M = OBE.time_dep_matrix(Omegas, Deltas, [G]) 24 | for i, t in enumerate(times): 25 | sol = OBE.time_evolve(M, t, rho_0) # perform the time evolution 26 | #extract the populations from the solution array 27 | rho_11, rho_22 = numpy.real(sol[0]), numpy.real(sol[3]) 28 | factor = rho_11 + rho_22 29 | popn = [rho_11, rho_22] 30 | pops[j,i,:] = popn/factor # store normalised populations in array 31 | 32 | # For a wider range of Gamma values, find the population at the steady state 33 | # (done by evaluating at a large value of t) 34 | more_Gammas = numpy.linspace(0.05,5.1,100) 35 | end_pops = numpy.zeros((len(more_Gammas),2), dtype = 'complex') 36 | t = 1000 37 | 38 | for j,G in enumerate(more_Gammas): 39 | M = OBE.time_dep_matrix(Omegas, Deltas, [G]) 40 | sol = OBE.time_evolve(M, t, rho_0) # perform the time evolution 41 | rho_11, rho_22 = numpy.real(sol[0]), numpy.real(sol[3]) 42 | factor = rho_11 + rho_22 43 | popn = [rho_11, rho_22] 44 | end_pops[j,:] = popn/factor # store normalised populations in array 45 | 46 | # Calculate the ratio of the populations 47 | ratios = numpy.real(end_pops[:,1]/end_pops[:,0]) 48 | 49 | fig, ax = pyplot.subplots(1,1,figsize = (7,3.5)) 50 | 51 | ax.plot(more_Gammas, ratios) 52 | ax.set_xlabel(r'$\Gamma/\Omega$') 53 | ax.set_ylabel(r'$\rho_{22}/\rho_{11}$') 54 | ax.set_xlim(numpy.min(more_Gammas), numpy.max(more_Gammas)) 55 | 56 | # Plot time evolution in an inset 57 | in_ax = inset_axes(ax, width = 4, height = 1.5, loc=1) 58 | for i in range(len(Gammas)): 59 | in_ax.plot(times/(2*numpy.pi), numpy.real(pops[i,:,0]), 60 | alpha = 1-norm_Gammas[i], c = 'C0') 61 | in_ax.set_xlabel(r'Time ($1/\Omega$)') 62 | in_ax.set_ylabel(r'$\rho_{11}$') 63 | in_ax.set_xlim(min(times/(2*numpy.pi)), max(times/(2*numpy.pi))) 64 | 65 | pyplot.subplots_adjust( 66 | top=0.9, 67 | bottom=0.2, 68 | left=0.08, 69 | right=0.98, 70 | hspace=0.2, 71 | wspace=0.22) 72 | pyplot.show() -------------------------------------------------------------------------------- /Figure_Codes/Figure_6.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes 4 | import OBE_Tools as OBE 5 | import scipy.stats as stats 6 | from scipy.optimize import curve_fit 7 | 8 | ## Plot the time taken for each solution method to complete a calculation for 9 | ## a varying number of atomic levels. 10 | 11 | n_levels = 13 12 | levels = numpy.arange(n_levels)+3 13 | 14 | #Import pre-calculated data (calculated using the Computation_Times.py file) 15 | matrix_times = numpy.genfromtxt('Matrix_Times.txt', delimiter = '\t') 16 | lsq_times = numpy.genfromtxt('Leastsq_Times.txt', delimiter = '\t') 17 | iterative_times = numpy.genfromtxt('Iterative_Times.txt', delimiter = '\t') 18 | 19 | # Find the average of 5 runs and the standard error on the mean for each method 20 | matrix_avg = numpy.mean(matrix_times, axis = 0) 21 | matrix_errs = stats.sem(matrix_times, axis = 0) 22 | 23 | iterative_avg = numpy.mean(iterative_times, axis = 0) 24 | iterative_errs = stats.sem(iterative_times, axis = 0) 25 | 26 | lsq_avg = numpy.mean(lsq_times, axis = 0) 27 | lsq_errs = stats.sem(lsq_times, axis = 0) 28 | 29 | def line(x, m, c): 30 | return m*x + c 31 | 32 | def power(x, a, b, c): 33 | return c + a*x**b 34 | 35 | # Fit a line to the analytic times and a power law to the matrix times 36 | power_guess = (0.05, 3.2,0) 37 | l_opt, l_cov = curve_fit(line, levels, iterative_avg) 38 | p_opt, p_cov = curve_fit(power, levels, matrix_avg, p0 = power_guess) 39 | p2_opt, p2_cov = curve_fit(power, levels, lsq_avg, p0 = power_guess) 40 | 41 | l_fit = line(levels, *l_opt) 42 | p_fit = power(levels, *p_opt) 43 | p2_fit = power(levels, *p2_opt) 44 | 45 | fig, ax = pyplot.subplots(1,1,figsize = (7,3.5)) 46 | ax.errorbar(levels, iterative_avg, yerr = iterative_errs, fmt = '.', capsize = 5) 47 | ax.errorbar(levels, matrix_avg, yerr = matrix_errs, fmt = '.', capsize = 5) 48 | ax.errorbar(levels, lsq_avg, yerr = lsq_errs, fmt = '.', capsize = 5, 49 | zorder = 0, mfc = 'white') 50 | ax.plot(levels, l_fit, c = 'C0', ls = 'dotted') 51 | ax.plot(levels, p_fit, c = 'C1', ls = 'dashed') 52 | #ax.plot(levels, p2_fit, c = 'C2', ls = (0,(1,10)), zorder = -1, alpha = 0.5) 53 | ax.set_xlabel(r'Number of levels, $n$') 54 | ax.set_ylabel('Time to solution (s)') 55 | ax.set_xlim(2.75,15.25) 56 | ax.set_xticks(levels) 57 | 58 | in_ax = inset_axes(ax, width = 3.1, height = 1.2, loc=2) 59 | in_ax.errorbar(levels, iterative_avg, yerr = iterative_errs, fmt = '.', capsize = 5) 60 | in_ax.plot(levels, l_fit, c = 'C0', ls = 'dotted') 61 | in_ax.set_ylim(0, 0.022) 62 | in_ax.yaxis.tick_right() 63 | in_ax.set_xlabel(r'Number of levels, $n$') 64 | in_ax.set_ylabel(r'Time (s)') 65 | in_ax.yaxis.set_label_position('right') 66 | pyplot.show() 67 | 68 | #print('Least squares ({} levels): {:.2f} s'.format(levels[-1], lsq_avg[-1])) 69 | #print('Matrix ({} levels): {:.2f} s'.format(levels[-1], matrix_avg[-1])) 70 | -------------------------------------------------------------------------------- /Figure_Codes/Figure_7b.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib.pyplot as pyplot 3 | import OBE_Tools as OBE 4 | 5 | ## Calculate the probe absorption as a function of probe detuning for different 6 | ## parameters, both with and without including dephasing. 7 | 8 | Omegas = [0.1,0] #2pi MHz 9 | Deltas = [0,0] #2pi MHz 10 | Gammas = [1,0.1] #2pi MHz 11 | 12 | Omega_23s = [0, 1, 5] 13 | gammas = [0.2,0.2] #2pi MHz 14 | 15 | Delta_12 = numpy.linspace(-5.8,5.8,400) #2pi MHz 16 | probe_abs = numpy.zeros((2, len(Omega_23s), len(Delta_12))) 17 | 18 | # for each value of Omega_23, find the probe absorption first without and then with dephasing 19 | for j, O in enumerate(Omega_23s): 20 | Omegas[1] = O 21 | for i, p in enumerate(Delta_12): 22 | Deltas[0] = p 23 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas) 24 | probe_abs[0,j,i] = numpy.imag(solution[1]) 25 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 26 | probe_abs[1,j,i] = numpy.imag(solution[1]) 27 | 28 | fig, axs = pyplot.subplots(1,2,figsize = (5,2.5), sharey = True) 29 | for i in range(len(Omega_23s)-1): 30 | ax = axs[i] 31 | ax.plot(Delta_12, probe_abs[0,0,:], label = r'$\Omega_{12} = 0$ MHz', 32 | c = 'C0', ls = 'dashed') 33 | ax.plot(Delta_12, probe_abs[0,i+1,:], label = r'$\Omega_{12} = 0$ MHz', 34 | c = 'C1') 35 | ax.plot(Delta_12, probe_abs[1,i+1, :], c = 'C2', ls='dotted') 36 | ax.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 37 | ax.set_xlim(min(Delta_12), max(Delta_12)) 38 | 39 | height = numpy.max(probe_abs[0,2,:]) 40 | scale = 1.07 41 | axs[1].plot((-Omega_23s[2]/2, Omega_23s[2]/2),(height*scale, height*scale), 42 | marker = '|', c = 'black', markeredgewidth = 1.5, lw = 1.5) 43 | axs[1].text(Omega_23s[2]/2 + 0.5, height*scale, r'$\Omega_{23}$', va = 'center') 44 | axs[0].set_ylabel(r'Probe absorption ($-\Im[\rho_{21}]$)') 45 | 46 | pyplot.subplots_adjust( 47 | top=0.9, 48 | bottom=0.2, 49 | left=0.12, 50 | right=0.98, 51 | hspace=0.2, 52 | wspace=0.05) 53 | 54 | pyplot.show() -------------------------------------------------------------------------------- /Figure_Codes/Figure_8.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib 3 | import matplotlib.pyplot as pyplot 4 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes 5 | from matplotlib.colors import ListedColormap, LinearSegmentedColormap 6 | import OBE_Tools as OBE 7 | 8 | ## Evaluate probe absorption as a function of probe detuning for varying values 9 | ## of detuning of the second field in a 3-level system. 10 | 11 | Omegas = [0.1,5] #2pi MHz 12 | Deltas = [0,0] #2pi MHz 13 | Gammas = [1,1] #2pi MHz 14 | 15 | gammas = [0.1,0.1] #2pi MHz 16 | 17 | Delta_12s = numpy.linspace(-9.8,9.8,600) #2pi MHz 18 | Delta_23s = numpy.linspace(0,10,12) 19 | probe_abs = numpy.zeros((len(Delta_23s), len(Delta_12s))) 20 | 21 | alphas = 1 - (Delta_23s/(numpy.max(Delta_23s)+0.5)) 22 | 23 | for j, c in enumerate(Delta_23s): 24 | Deltas[1] = c 25 | for i, p in enumerate(Delta_12s): 26 | Deltas[0] = p 27 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 28 | probe_abs[j,i] = numpy.imag(solution[1]) 29 | 30 | fig = pyplot.figure(figsize = (7,3)) 31 | ax1 = pyplot.subplot2grid((1,1), (0,0)) 32 | 33 | for i in range(len(Delta_23s)): 34 | ax1.plot(Delta_12s, probe_abs[i,:], c = 'C0', alpha = alphas[i]) 35 | 36 | ax1.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 37 | 38 | ax1.set_xlim(min(Delta_12s), max(Delta_12s)) 39 | ax1.set_ylabel(r'Probe absorption ($-\Im[\rho_{21}]$)') 40 | 41 | in_ax = inset_axes(ax1, width = '30%', height = "15%", loc = 'upper right') 42 | colors = ["C0", "white"] 43 | cmap1 = LinearSegmentedColormap.from_list("mycmap", colors) 44 | norm = matplotlib.colors.Normalize(vmin=0, vmax=10) 45 | fig.colorbar(matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap1), 46 | cax=in_ax, orientation='horizontal', label=r'$\Delta_{23}$ (MHz)', 47 | ticks = [2,4,6,8]) 48 | 49 | pyplot.subplots_adjust( 50 | top=0.95, 51 | bottom=0.16, 52 | left=0.1, 53 | right=0.98, 54 | hspace=0.32, 55 | wspace=0.05) 56 | pyplot.show() -------------------------------------------------------------------------------- /Figure_Codes/Figure_9.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import matplotlib 3 | import matplotlib.pyplot as pyplot 4 | from matplotlib.lines import Line2D 5 | import OBE_Tools as OBE 6 | 7 | ## Calculate state populations in the steady-state as a function of probe 8 | ## detuning for different values of probe Rabi frequency. 9 | 10 | Omegas = [0.1,4] #2pi MHz 11 | Deltas = [0,0] #2pi MHz 12 | Gammas = [0.5,0.2] #2pi MHz 13 | 14 | Omega_12s = numpy.linspace(0.1,2.5,10) 15 | gammas = [0.1,0.1] #2pi MHz 16 | 17 | Delta_12s = numpy.linspace(-5.8,5.8,200) #2pi MHz 18 | pops = numpy.zeros((3, len(Omega_12s), len(Delta_12s))) 19 | 20 | alphas = numpy.linspace(1,0.05,len(Omega_12s)) #line opacity for plot 21 | 22 | # for each value of Omega_12 and Delta_12, find the steady-state population and store in array 23 | for j, O in enumerate(Omega_12s): 24 | Omegas[0] = O 25 | for i, p in enumerate(Delta_12s): 26 | Deltas[0] = p 27 | solution = OBE.steady_state_soln(Omegas, Deltas, Gammas, gammas = gammas) 28 | for p in range(3): 29 | pops[p,j,i] = numpy.real(solution[4*p]) 30 | 31 | colours = ['C0', 'C2', 'C3'] 32 | lines = ['solid', 'dashed', 'dotted'] 33 | fig = pyplot.figure(figsize = (7,3)) 34 | ax1 = pyplot.subplot2grid((1,1), (0,0)) 35 | 36 | for i in range(len(Omega_12s)): 37 | for p in range(3): 38 | ax1.plot(Delta_12s, pops[p,i,:], c = colours[p], alpha = alphas[i], ls = lines[p]) 39 | 40 | ax1.set_xlabel('Probe detuning $\Delta_{12}$ (MHz)') 41 | 42 | ax1.set_xlim(min(Delta_12s), max(Delta_12s)) 43 | ax1.set_ylabel(r'State population') 44 | 45 | custom_lines = [Line2D([0], [0], color='C0', lw=2, ls = 'solid'), 46 | Line2D([0], [0], color='C2', lw=2, ls = 'dashed'), 47 | Line2D([0], [0], color='C3', lw=2, ls = 'dotted')] 48 | 49 | ax1.legend(custom_lines, [r'$|1\rangle$', r'$|2\rangle$', r'$|3\rangle$'], loc = 0) 50 | 51 | pyplot.subplots_adjust( 52 | top=0.95, 53 | bottom=0.16, 54 | left=0.1, 55 | right=0.98, 56 | hspace=0.32, 57 | wspace=0.05) 58 | pyplot.show() 59 | 60 | -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_10.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_11.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_12.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_14.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_15.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_2.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_3.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_6.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_7b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_7b.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_8.png -------------------------------------------------------------------------------- /Figure_Codes/Figures/Fig_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucyDownes/OBE_Python_Tools/a2016f4410f68d4a80d66ce2ca7d3cc60f7b9ccc/Figure_Codes/Figures/Fig_9.png -------------------------------------------------------------------------------- /Figure_Codes/Iterative_Times.txt: -------------------------------------------------------------------------------- 1 | 2.990245819091796875e-03 3.989219665527343750e-03 5.983352661132812500e-03 8.010864257812500000e-03 8.974790573120117188e-03 1.093816757202148438e-02 1.097106933593750000e-02 1.296567916870117188e-02 1.396536827087402344e-02 1.595687866210937500e-02 1.795887947082519531e-02 1.894950866699218750e-02 1.994585990905761719e-02 2 | 2.991914749145507812e-03 3.994226455688476562e-03 5.980730056762695312e-03 6.980657577514648438e-03 8.975744247436523438e-03 9.973287582397460938e-03 1.097059249877929688e-02 1.296472549438476562e-02 1.496005058288574219e-02 1.596355438232421875e-02 1.695418357849121094e-02 1.795029640197753906e-02 1.994538307189941406e-02 3 | 2.991676330566406250e-03 4.986524581909179688e-03 5.984067916870117188e-03 7.978439331054687500e-03 8.975267410278320312e-03 9.972572326660156250e-03 1.197147369384765625e-02 1.297092437744140625e-02 1.396226882934570312e-02 1.595735549926757812e-02 1.795196533203125000e-02 1.795125007629394531e-02 2.094268798828125000e-02 4 | 2.991914749145507812e-03 3.989696502685546875e-03 6.981849670410156250e-03 6.982088088989257812e-03 7.978200912475585938e-03 9.972810745239257812e-03 1.193547248840332031e-02 1.296520233154296875e-02 1.398944854736328125e-02 1.595211029052734375e-02 1.795244216918945312e-02 1.894807815551757812e-02 2.097773551940917969e-02 5 | 3.988981246948242188e-03 5.022287368774414062e-03 4.986286163330078125e-03 6.988525390625000000e-03 7.978916168212890625e-03 9.972810745239257812e-03 1.193547248840332031e-02 1.296520233154296875e-02 1.495981216430664062e-02 1.595735549926757812e-02 1.796007156372070312e-02 1.894855499267578125e-02 1.994657516479492188e-02 6 | -------------------------------------------------------------------------------- /Figure_Codes/LeastSq_Times.txt: -------------------------------------------------------------------------------- 1 | 2.627481460571289062e+00 5.790467262268066406e+00 1.040187191963195801e+01 2.317812681198120117e+01 3.615863847732543945e+01 5.508819794654846191e+01 7.656228494644165039e+01 1.061698193550109863e+02 1.462183623313903809e+02 1.961128280162811279e+02 2.569752228260040283e+02 3.339984602928161621e+02 4.258115222454071045e+02 2 | 2.614135980606079102e+00 5.411526918411254883e+00 9.967312812805175781e+00 2.211035609245300293e+01 3.428744554519653320e+01 5.256698417663574219e+01 7.639320874214172363e+01 1.060750308036804199e+02 1.456050372123718262e+02 1.956073741912841797e+02 2.573987543582916260e+02 3.323599345684051514e+02 4.275265786647796631e+02 3 | 2.623112678527832031e+00 5.466551065444946289e+00 9.874100923538208008e+00 2.215644454956054688e+01 3.419332170486450195e+01 5.246701025962829590e+01 7.669004702568054199e+01 1.086453719139099121e+02 1.450448894500732422e+02 1.966152670383453369e+02 2.577081952095031738e+02 3.328967628479003906e+02 4.261060404777526855e+02 4 | 2.635143280029296875e+00 5.347721576690673828e+00 9.859143257141113281e+00 2.192655205726623535e+01 3.420278048515319824e+01 5.235291600227355957e+01 7.634018993377685547e+01 1.061714184284210205e+02 1.451154823303222656e+02 1.958622715473175049e+02 2.572988836765289307e+02 3.328063023090362549e+02 4.259243507385253906e+02 5 | 2.624089956283569336e+00 5.341266632080078125e+00 9.988038063049316406e+00 2.194596791267395020e+01 3.427279853820800781e+01 5.299893069267272949e+01 7.692588877677917480e+01 1.059456682205200195e+02 1.458320121765136719e+02 1.965255789756774902e+02 2.573767197132110596e+02 3.358826956748962402e+02 4.250834188461303711e+02 6 | -------------------------------------------------------------------------------- /Figure_Codes/Matrix_Times.txt: -------------------------------------------------------------------------------- 1 | 2.819917678833007812e+00 5.421704292297363281e+00 1.070568943023681641e+01 2.439068222045898438e+01 3.799397182464599609e+01 5.889068460464477539e+01 8.300180125236511230e+01 1.104853532314300537e+02 1.519830083847045898e+02 2.032121133804321289e+02 2.666261389255523682e+02 3.430848093032836914e+02 4.408365745544433594e+02 2 | 2.603351354598999023e+00 5.284505367279052734e+00 1.010125494003295898e+01 2.323443913459777832e+01 3.618777418136596680e+01 5.604957604408264160e+01 7.966575074195861816e+01 1.104072818756103516e+02 1.511616380214691162e+02 2.046537461280822754e+02 2.661073515415191650e+02 3.435284070968627930e+02 4.370447969436645508e+02 3 | 2.614597797393798828e+00 5.112162113189697266e+00 9.969924211502075195e+00 2.337748432159423828e+01 3.652780389785766602e+01 5.574588203430175781e+01 8.023006176948547363e+01 1.103257207870483398e+02 1.517807068824768066e+02 2.034755225181579590e+02 2.669083456993103027e+02 3.432188739776611328e+02 4.358109283447265625e+02 4 | 2.633061170578002930e+00 5.198838472366333008e+00 1.023792481422424316e+01 2.325740194320678711e+01 3.662462854385375977e+01 5.596310639381408691e+01 8.021333479881286621e+01 1.103201153278350830e+02 1.511851875782012939e+02 2.041048817634582520e+02 2.667829074859619141e+02 3.432372474670410156e+02 4.350054619312286377e+02 5 | 2.615223169326782227e+00 5.085937976837158203e+00 1.000152993202209473e+01 2.313012123107910156e+01 3.643246436119079590e+01 5.691517090797424316e+01 7.993010234832763672e+01 1.108816101551055908e+02 1.515472762584686279e+02 2.036444098949432373e+02 2.667532441616058350e+02 3.428794443607330322e+02 4.358588991165161133e+02 6 | -------------------------------------------------------------------------------- /Functions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 55, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy\n", 10 | "import sympy\n", 11 | "from scipy import constants, stats, linalg" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# Simple Python tools for modelling few-level atom-light interactions\n", 19 | "\n", 20 | "This notebook outlines all the functions used by the `OBE_Tools` module to find solutions of the optical Bloch equations for an arbitrary number of levels both within and beyond the weak-probe regime. Different solution methods are available for different situations. We also provide some functions for looking at the time evolution of the system, and performing Doppler averaging in thermal ensembles. Examples of using the functions outlined here can be found in the \"Examples\" notebook. More information on the physics can be found in the paper.\n", 21 | "\n", 22 | "\n", 23 | "### Notes\n", 24 | "\n", 25 | "- $\\hbar = 1$\n", 26 | "- Parameter/variable names are chosen to match the notation in the paper. Parameters referring to fields that couple states $i$ and $j$ have the subscript $ij$, for example $\\Omega_{ij}$, `Omega_ij`. Parameters that are related to an individual atomic state $|n\\rangle$ have a single subscript $n$ such as $\\Gamma_n$, `Gamma_n`. Variables with upper/lower-case letters refer to parameters denoted by upper/lower-case Greek letters respectively, for example `Gamma` denotes $\\Gamma$ whereas `gamma` denotes $\\gamma$.\n", 27 | "\n", 28 | "The code makes a number of assumptions:\n", 29 | "- That the number of atomic levels is equal to 1 plus the number of fields. This is consistent with a ladder system in which each level is coupled to the ones above and below it (except for the top and bottom levels).\n", 30 | "- That each level only decays to the level below it, and there is no decay out of the bottom level. \n", 31 | "- That laser linewidths ($\\gamma$) do not matter (unless they are explicitly specified).\n", 32 | "\n", 33 | "\n", 34 | "# Introduction\n", 35 | "\n", 36 | "Often in modelling dynamics of atom-light systems it is useful to be able to numerically solve the optical Bloch equations (OBEs), especially since for systems with more than 2 levels the analytic solutions are difficult to derive and require significant approximations. The OBEs arise from using the Master equation to find the time dependence of the density matrix through\n", 37 | "\n", 38 | "$$\\frac{\\partial \\hat{\\rho}}{\\partial t} = -\\frac{i}{\\hbar}\\left[\\hat{H}, \\hat{\\rho}\\right] + \\hat{\\mathcal{L}}(\\hat{\\rho})$$\n", 39 | "\n", 40 | "where $\\hat{H}$ is the Hamiltonian of the atom-light system, $\\hat{\\rho}$ is the density matrix and $\\hat{\\mathcal{L}}$ describes the decay/dephasing in the system.\n", 41 | "\n", 42 | "\n", 43 | "\n", 44 | "# Steady-state solutions\n", 45 | "\n", 46 | " \n", 47 | "\n", 48 | "## Matrix Solution\n", 49 | "\n", 50 | "This solution method uses `Sympy` to derive the OBEs from an interaction Hamiltonian and decay/dephasing operator, then expresses these as matrix equations and uses a singular value decomposition to solve them. It makes no assumptions about the relative strengths of the field so is valid both within and beyond the weak-probe regime.\n", 51 | "\n", 52 | "First we set up two functions to create a list of the components of the density matrix $\\rho_{i,j}$ and then set up the density matrix $\\hat{\\rho}$ itself. \n", 53 | "These functions return a list/matrix populated with `Sympy` symbolic objects which will allow us to set up the equations and manipulate the terms." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 3, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "def create_rho_list(levels = 3):\n", 63 | " rho_list = []\n", 64 | " for i in range(levels):\n", 65 | " for j in range(levels):\n", 66 | " globals()['rho_'+str(i+1)+str(j+1)] = sympy.Symbol(\\\n", 67 | " 'rho_'+str(i+1)+str(j+1))\n", 68 | " rho_list.append(globals()['rho_'+str(i+1)+str(j+1)])\n", 69 | " return rho_list" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 9, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "name": "stdout", 79 | "output_type": "stream", 80 | "text": [ 81 | "[rho_11, rho_12, rho_13, rho_21, rho_22, rho_23, rho_31, rho_32, rho_33]\n", 82 | "Type: \n", 83 | "Length: 9\n" 84 | ] 85 | } 86 | ], 87 | "source": [ 88 | "print(create_rho_list())\n", 89 | "print('Type: {}'.format(type(create_rho_list())))\n", 90 | "print('Length: {}'.format(len(create_rho_list())))" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "For an n-level system the density matrix will have $n^2$ components. For a 2-level system with ground state $|1\\rangle$ and excited state $|2\\rangle$ these components would be $\\rho_{11}, \\rho_{12}, \\rho_{21}$ and $\\rho_{22}$. The usefulness of creating a list instead of the properly structured $n\\times n$ matrix is purely for ease of referring to particular elements later in the code.\n", 98 | "\n", 99 | "Similarly if we put these components into the usual density matrix structure we get $$\\hat{\\rho} = \\begin{pmatrix} \\rho_{11} & \\rho_{12}\\\\ \\rho_{21} & \\rho_{22} \\end{pmatrix}$$\n", 100 | "\n", 101 | "Again this matrix is populated with Sympy symbolic objects." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 11, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "def create_rho_matrix(levels = 3):\n", 111 | " rho_matrix = numpy.empty((levels, levels), dtype = 'object')\n", 112 | " for i in range(levels):\n", 113 | " for j in range(levels):\n", 114 | " globals()['rho_'+str(i+1)+str(j+1)] = \\\n", 115 | " sympy.Symbol('rho_'+str(i+1)+str(j+1))\n", 116 | " rho_matrix[i,j] = globals()['rho_'+str(i+1)+str(j+1)]\n", 117 | " return numpy.matrix(rho_matrix)" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 13, 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | "[[rho_11 rho_12 rho_13]\n", 130 | " [rho_21 rho_22 rho_23]\n", 131 | " [rho_31 rho_32 rho_33]]\n", 132 | "Type: \n", 133 | "Shape: (3, 3)\n" 134 | ] 135 | } 136 | ], 137 | "source": [ 138 | "print(create_rho_matrix())\n", 139 | "print('Type: {}'.format(type(create_rho_matrix())))\n", 140 | "print('Shape: {}'.format(create_rho_matrix().shape))" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "Next we set up the Hamiltonian of the interaction between the atom and the light. If we have a simple 2-level system where the levels are coupled by a field with Rabi frequency $\\Omega_{12}$ which is detuned from resonance by $\\Delta_{12}$ then (ignoring $\\hbar$) our Hamiltonian will look like \n", 148 | "$$\\hat{H} = \\frac{1}{2} \\begin{pmatrix} 0 & \\Omega_{12} \\\\ \\Omega_{12} & -2\\Delta_{12} \\end{pmatrix}$$\n", 149 | "\n", 150 | "Similarly for a 3-level system where levels $|1\\rangle$ and $|2\\rangle$ are coupled by $\\Omega_{12}$ with detuning $\\Delta_{12}$ and levels $|2\\rangle$ and $|3\\rangle$ are coupled by $\\Omega_{23}$ with detuning $\\Delta_{23}$, our Hamiltonian will be\n", 151 | "$$ \\hat{H} = \\frac{1}{2} \\begin{pmatrix} 0 & \\Omega_{12} & 0 \\\\ \\Omega_{12} & -2\\Delta_{12} & \\Omega_{23} \\\\ 0 & \\Omega_{23} & -2(\\Delta_{12} + \\Delta_{23}) \\end{pmatrix}$$" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 14, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "def Hamiltonian(Omegas, Deltas):\n", 161 | " levels = len(Omegas)+1\n", 162 | " H = numpy.zeros((levels,levels))\n", 163 | " for i in range(levels):\n", 164 | " for j in range(levels):\n", 165 | " if numpy.logical_and(i==j, i!=0):\n", 166 | " H[i,j] = -2*(numpy.sum(Deltas[:i]))\n", 167 | " elif numpy.abs(i-j) == 1:\n", 168 | " H[i,j] = Omegas[numpy.min([i,j])]\n", 169 | " return numpy.matrix(H/2)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "This function will set up the Hamiltonian for any n-level ladder system where each level is only coupled to the levels directly above and below it (ladder system). The values of the Rabi frequencies $\\Omega_{ij}$ and detunings $\\Delta_{ij}$ of each field must be supplied as lists/arrays of the same length (length $n - 1$ for an n-level system). " 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 15, 182 | "metadata": {}, 183 | "outputs": [ 184 | { 185 | "name": "stdout", 186 | "output_type": "stream", 187 | "text": [ 188 | "[[ 0. 0.5 0. ]\n", 189 | " [ 0.5 -2. 1.5]\n", 190 | " [ 0. 1.5 -6. ]]\n", 191 | "Type: \n", 192 | "Shape: (3, 3)\n" 193 | ] 194 | } 195 | ], 196 | "source": [ 197 | "Omegas = [1,3]\n", 198 | "Deltas = [2,4]\n", 199 | "H = Hamiltonian(Omegas, Deltas)\n", 200 | "print(H)\n", 201 | "print('Type: {}'.format(type(H)))\n", 202 | "print('Shape: {}'.format(H.shape))" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "### Decay and Dephasing\n", 210 | "\n", 211 | "Next we set up the matrix form of the decay operator $\\hat{\\mathcal{L}}$. We will split this into two separate parts for simplicity: one part describing the atomic decay $\\hat{L}_{\\rm{atom}}$ and another describing dephasing due to the finite linewidths of the driving fields $\\hat{L}_{\\rm{dephasing}}$. The total operator will then be the sum of these parts $\\hat{\\mathcal{L}} = \\hat{L}_{\\rm{atom}} + \\hat{L}_{\\rm{dephasing}}$.\n", 212 | "\n", 213 | "First we focus on the Linblad superoperator $\\hat{L}_{\\rm{atom}}$ describing spontaneous decay. For this simple approach we assume that each level can only decay to the level below it at a rate $\\Gamma_n$, and that there is no decay out of the bottom level. This means that the coherences (off-diagonal elements) will be as $L_{ij,\\, i\\neq j} = -\\frac{\\Gamma_i + \\Gamma_j}{2}\\rho_{ij}$ while the populations (diagonal elements) will be given by $L_{ij, \\, i=j} = \\Gamma_{i+1}\\rho_{i+1, j+1} - \\Gamma_{i}\\rho_{ij}$. This means that for a 3-level system we have\n", 214 | "\n", 215 | "$$\\hat{L}_{\\rm{atom}} = \\begin{pmatrix} \n", 216 | "\\Gamma_2 \\rho_{22} - \\Gamma_1\\rho_{11} & - \\frac{\\Gamma_1 + \\Gamma_2}{2}\\rho_{12} & -\\frac{\\Gamma_1 + \\Gamma_3}{2}\\rho_{13} \\\\ \n", 217 | "-\\frac{\\Gamma_2 + \\Gamma_1}{2}\\rho_{21} & \\Gamma_3 \\rho_{33} - \\Gamma_2\\rho_{22} & -\\frac{\\Gamma_2 + \\Gamma_3}{2}\\rho_{23} \\\\\n", 218 | "-\\frac{\\Gamma_3 + \\Gamma_1}{2}\\rho_{31} & -\\frac{\\Gamma_3 + \\Gamma_2}{2}\\rho_{32} & - \\Gamma_3\\rho_{33} \\end{pmatrix}$$\n", 219 | "Making the assumption that there is no decay out of the lower level ($\\Gamma_1 = 0$) simplifies the expression to \n", 220 | "$$\\hat{L}_{\\rm{atom}} = \\begin{pmatrix} \n", 221 | "\\Gamma_2 \\rho_{22} & - \\frac{\\Gamma_2}{2}\\rho_{12} & -\\frac{\\Gamma_3}{2}\\rho_{13} \\\\ \n", 222 | "-\\frac{\\Gamma_2}{2}\\rho_{21} & \\Gamma_3 \\rho_{33} - \\Gamma_2\\rho_{22} & -\\frac{\\Gamma_2 + \\Gamma_3}{2}\\rho_{23} \\\\\n", 223 | "-\\frac{\\Gamma_3}{2}\\rho_{31} & -\\frac{\\Gamma_3 + \\Gamma_2}{2}\\rho_{32} & - \\Gamma_3\\rho_{33} \\end{pmatrix}$$" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 16, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "def L_decay(Gammas):\n", 233 | " levels = len(Gammas)+1\n", 234 | " rhos = create_rho_matrix(levels = levels)\n", 235 | " Gammas_all = [0] + Gammas\n", 236 | " decay_matrix = numpy.zeros((levels, levels), dtype = 'object')\n", 237 | " for i in range(levels):\n", 238 | " for j in range(levels):\n", 239 | " if i != j:\n", 240 | " decay_matrix[i,j] = -0.5*(Gammas_all[i]\\\n", 241 | " +Gammas_all[j])*rhos[i,j]\n", 242 | " elif i != levels - 1:\n", 243 | " into = Gammas_all[i+1]*rhos[1+i, j+1]\n", 244 | " outof = Gammas_all[i]*rhos[i, j]\n", 245 | " decay_matrix[i,j] = into - outof\n", 246 | " else:\n", 247 | " outof = Gammas_all[i]*rhos[i, j]\n", 248 | " decay_matrix[i,j] = - outof\n", 249 | " return numpy.matrix(decay_matrix)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": 17, 255 | "metadata": {}, 256 | "outputs": [ 257 | { 258 | "name": "stdout", 259 | "output_type": "stream", 260 | "text": [ 261 | "[[2*rho_22 -1.0*rho_12 -0.5*rho_13]\n", 262 | " [-1.0*rho_21 -2*rho_22 + rho_33 -1.5*rho_23]\n", 263 | " [-0.5*rho_31 -1.5*rho_32 -rho_33]]\n", 264 | "Type: \n", 265 | "Shape: (3, 3)\n" 266 | ] 267 | } 268 | ], 269 | "source": [ 270 | "Gammas = [2,1]\n", 271 | "L_atom = L_decay(Gammas)\n", 272 | "print(L_atom)\n", 273 | "print('Type: {}'.format(type(L_atom)))\n", 274 | "print('Shape: {}'.format(L_atom.shape))" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "Again this function returns an $n\\times n$ matrix populated by multiples of Sympy symbolic objects. \n", 282 | "\n", 283 | "The dephasing due to the laser linewidths is generally a smaller effect, so can often be neglected. If we do include it, it only affects the coherences (off-diagonal density matrix elements). Following the treatment in Gea-Banacloche et al, the elements of the dephasing matrix can be expressed as\n", 284 | "$$L_{ij, i\\neq j} = -\\left( \\gamma_{i,i+1} + \\dots + \\gamma_{j-1, j}\\right) \\rho_{ij}$$ where $\\gamma_{n,n+1}$ is the linewidth of the field coupling the $n$ and $n+1$ levels." 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": 19, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "def L_dephasing(gammas):\n", 294 | " levels = len(gammas)+1\n", 295 | " rhos = create_rho_matrix(levels = levels)\n", 296 | " deph_matrix = numpy.zeros((levels, levels), dtype = 'object')\n", 297 | " for i in range(levels):\n", 298 | " for j in range(levels):\n", 299 | " if i != j:\n", 300 | " deph_matrix[i,j] = -(numpy.sum(\\\n", 301 | " gammas[numpy.min([i,j]):numpy.max([i,j])]))*rhos[i,j]\n", 302 | " return numpy.matrix(deph_matrix)" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": 21, 308 | "metadata": {}, 309 | "outputs": [ 310 | { 311 | "name": "stdout", 312 | "output_type": "stream", 313 | "text": [ 314 | "[[0 -rho_12 -2*rho_13]\n", 315 | " [-rho_21 0 -rho_23]\n", 316 | " [-2*rho_31 -rho_32 0]]\n", 317 | "Type: \n", 318 | "Shape: (3, 3)\n" 319 | ] 320 | } 321 | ], 322 | "source": [ 323 | "gammas = [1,1]\n", 324 | "L_laser = L_dephasing(gammas)\n", 325 | "print(L_laser)\n", 326 | "print('Type: {}'.format(type(L_laser)))\n", 327 | "print('Shape: {}'.format(L_laser.shape))" 328 | ] 329 | }, 330 | { 331 | "cell_type": "markdown", 332 | "metadata": {}, 333 | "source": [ 334 | "Now we have expressions for the Hamiltonian and Linbladian, we can put them into the Master equation to get an expression for the time evolution of the density matrix. We have that \n", 335 | "$$ \\frac{\\partial \\hat{\\rho}}{\\partial t} = -i(\\hat{H}\\hat{\\rho} - \\hat{\\rho}\\hat{H}) + \\hat{L}_{\\rm{atom}} + \\hat{L}_{\\rm{dephasing}}$$" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": 22, 341 | "metadata": {}, 342 | "outputs": [], 343 | "source": [ 344 | "def Master_eqn(H_tot, L):\n", 345 | " levels = H_tot.shape[0]\n", 346 | " rho = create_rho_matrix(levels = levels)\n", 347 | " return -1j*(H_tot*rho - rho*H_tot) + L" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 23, 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "name": "stdout", 357 | "output_type": "stream", 358 | "text": [ 359 | "[[2*rho_22 - 1.0*I*(-0.5*rho_12 + 0.5*rho_21)\n", 360 | " -2.0*rho_12 - 1.0*I*(-0.5*rho_11 + 2.0*rho_12 - 1.5*rho_13 + 0.5*rho_22)\n", 361 | " -2.5*rho_13 - 1.0*I*(-1.5*rho_12 + 6.0*rho_13 + 0.5*rho_23)]\n", 362 | " [-2.0*rho_21 - 1.0*I*(0.5*rho_11 - 2.0*rho_21 - 0.5*rho_22 + 1.5*rho_31)\n", 363 | " -2*rho_22 + rho_33 - 1.0*I*(0.5*rho_12 - 0.5*rho_21 - 1.5*rho_23 + 1.5*rho_32)\n", 364 | " -2.5*rho_23 - 1.0*I*(0.5*rho_13 - 1.5*rho_22 + 4.0*rho_23 + 1.5*rho_33)]\n", 365 | " [-2.5*rho_31 - 1.0*I*(1.5*rho_21 - 6.0*rho_31 - 0.5*rho_32)\n", 366 | " -2.5*rho_32 - 1.0*I*(1.5*rho_22 - 0.5*rho_31 - 4.0*rho_32 - 1.5*rho_33)\n", 367 | " -rho_33 - 1.0*I*(1.5*rho_23 - 1.5*rho_32)]]\n", 368 | "Type: \n", 369 | "Shape: (3, 3)\n" 370 | ] 371 | } 372 | ], 373 | "source": [ 374 | "L = L_atom + L_laser\n", 375 | "master_rhs = Master_eqn(H,L)\n", 376 | "print(master_rhs)\n", 377 | "print('Type: {}'.format(type(master_rhs)))\n", 378 | "print('Shape: {}'.format(master_rhs.shape))" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "metadata": {}, 384 | "source": [ 385 | "We now have an expression for the time evolution of each of the components of the density matrix in terms of other components. \n", 386 | "To make this complex system of equations more convenient to solve we want to write them in the form \n", 387 | "$$\\frac{\\partial \\hat{\\rho}_{\\rm{vect}}}{\\partial t} = \\hat{M}\\hat{\\rho}_{\\rm{vect}}$$\n", 388 | "where $\\hat{\\rho}_{\\rm{vect}}$ is a column vector containing all of the elements of $\\hat{\\rho}$ in the order returned by `create_rho_list`, and $\\hat{M}$ is a matrix of coeficients. \n", 389 | "\n", 390 | "We can create this matrix of coefficients $\\hat{M}$ by using the `Sympy` functions `expand` and `coeff`. The `expand` function collects together terms containing the same symbolic object. The `coeff` function allows us to extract the multiplying coefficient for a specific symbolic object. (This works since the only terms we have are linear with respect to the density matrix elements, e.g. we never have terms in $\\rho^2$ or higher)" 391 | ] 392 | }, 393 | { 394 | "cell_type": "code", 395 | "execution_count": 24, 396 | "metadata": {}, 397 | "outputs": [], 398 | "source": [ 399 | "def OBE_matrix(Master_matrix):\n", 400 | " levels = Master_matrix.shape[0]\n", 401 | " rho_vector = create_rho_list(levels = levels)\n", 402 | " coeff_matrix = numpy.zeros((levels**2, levels**2), dtype = 'complex')\n", 403 | " count = 0\n", 404 | " for i in range(levels):\n", 405 | " for j in range(levels):\n", 406 | " entry = Master_matrix[i,j]\n", 407 | " expanded = sympy.expand(entry)\n", 408 | " for n,r in enumerate(rho_vector):\n", 409 | " coeff_matrix[count, n] = complex(expanded.coeff(r))\n", 410 | " count += 1\n", 411 | " return coeff_matrix" 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": 25, 417 | "metadata": {}, 418 | "outputs": [ 419 | { 420 | "name": "stdout", 421 | "output_type": "stream", 422 | "text": [ 423 | "[[ 0. +0.j 0. +0.5j 0. +0.j 0. -0.5j 2. +0.j 0. +0.j 0. +0.j\n", 424 | " 0. +0.j 0. +0.j ]\n", 425 | " [ 0. +0.5j -2. -2.j 0. +1.5j 0. +0.j 0. -0.5j 0. +0.j 0. +0.j\n", 426 | " 0. +0.j 0. +0.j ]\n", 427 | " [ 0. +0.j 0. +1.5j -2.5-6.j 0. +0.j 0. +0.j 0. -0.5j 0. +0.j\n", 428 | " 0. +0.j 0. +0.j ]\n", 429 | " [ 0. -0.5j 0. +0.j 0. +0.j -2. +2.j 0. +0.5j 0. +0.j 0. -1.5j\n", 430 | " 0. +0.j 0. +0.j ]\n", 431 | " [ 0. +0.j 0. -0.5j 0. +0.j 0. +0.5j -2. +0.j 0. +1.5j 0. +0.j\n", 432 | " 0. -1.5j 1. +0.j ]\n", 433 | " [ 0. +0.j 0. +0.j 0. -0.5j 0. +0.j 0. +1.5j -2.5-4.j 0. +0.j\n", 434 | " 0. +0.j 0. -1.5j]\n", 435 | " [ 0. +0.j 0. +0.j 0. +0.j 0. -1.5j 0. +0.j 0. +0.j -2.5+6.j\n", 436 | " 0. +0.5j 0. +0.j ]\n", 437 | " [ 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. -1.5j 0. +0.j 0. +0.5j\n", 438 | " -2.5+4.j 0. +1.5j]\n", 439 | " [ 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. -1.5j 0. +0.j\n", 440 | " 0. +1.5j -1. +0.j ]]\n", 441 | "Type: \n", 442 | "Shape: (9, 9)\n" 443 | ] 444 | } 445 | ], 446 | "source": [ 447 | "coeffs = OBE_matrix(master_rhs)\n", 448 | "print(coeffs)\n", 449 | "print('Type: {}'.format(type(coeffs)))\n", 450 | "print('Shape: {}'.format(coeffs.shape))" 451 | ] 452 | }, 453 | { 454 | "cell_type": "markdown", 455 | "metadata": {}, 456 | "source": [ 457 | "### Singular Value Decomposition\n", 458 | "\n", 459 | "We now have an expression for $\\frac{\\partial \\hat{\\rho}_{\\rm{vect}}}{\\partial t}$ in terms of the matrix $\\hat{M}$ multiplied by $\\hat{\\rho}_{\\rm{vect}}$. In the steady state, $\\frac{\\partial \\hat{\\rho}_{\\rm{vect}}}{\\partial t} = 0$ so we need to solve the expression \n", 460 | "$$\\hat{M}\\hat{\\rho}_{\\rm{vect}} = 0.$$\n", 461 | "\n", 462 | "This can be solved numerically by performing a singular value decomposition (SVD) on the matrix $\\hat{M}$. The full details of the theory of the SVD are beyond this discussion, it is suffice to say that the SVD transforms $\\hat{M}$ into three matrices such that $$\\hat{M} = U\\Sigma V^{*},$$ where $V^{*}$ denotes the conjugate transpose of $V$. The matrix $\\Sigma$ is diagonal, the elements corresponding to the singular values of $\\hat{M}$. The solution to the system of equations is then the column of $V$ corresponding to the zero singular value. Since we have the same number of equations as we have unknowns we expect only one non-trivial solution. If none of the singular values are zero ($\\hat{M}$ is non-singular) then there is no non-trivial solution.\n", 463 | "\n", 464 | "We perform the SVD using the built-in `numpy.linalg.svd` routine which returns the matrices $U$ and $V^*$ along with an array of singular values $\\Sigma$. Before returning the solution we normalise the sum of the populations to be 1. The function returns an array of values for $\\hat{\\rho}_{\\rm{vect}}$, the order of which is the same as the elements of the output of the `create_rho_list` function." 465 | ] 466 | }, 467 | { 468 | "cell_type": "code", 469 | "execution_count": 26, 470 | "metadata": {}, 471 | "outputs": [], 472 | "source": [ 473 | "def SVD(coeff_matrix):\n", 474 | " levels = int(numpy.sqrt(coeff_matrix.shape[0]))\n", 475 | " u,sig,v = numpy.linalg.svd(coeff_matrix) # perform the singular value decomposition\n", 476 | " abs_sig = numpy.abs(sig)\n", 477 | " minval = numpy.min(abs_sig)\n", 478 | " if minval>1e-12:\n", 479 | " # if there is no zero singular value (within floating point tolerance)\n", 480 | " # return an empty array\n", 481 | " print('ERROR - Matrix is non-singular')\n", 482 | " return numpy.zeros((levels**2))\n", 483 | " index = abs_sig.argmin() # find the position of the zero solution\n", 484 | " rho = numpy.conjugate(v[index,:]) # extract the solution, which is the column \n", 485 | " # of V at the position of the zero singular value\n", 486 | " pops = numpy.zeros((levels))\n", 487 | " for l in range(levels):\n", 488 | " pops[l] = numpy.real(rho[(l*levels)+l]) # extract the terms of \\rho \n", 489 | " # corresponding to populations (e.g. rho_11, rho_22 etc.)\n", 490 | " t = 1/(numpy.sum(pops))\n", 491 | " rho_norm = rho*t.item() #normalise such that the populations sum to 1\n", 492 | " return rho_norm" 493 | ] 494 | }, 495 | { 496 | "cell_type": "code", 497 | "execution_count": 27, 498 | "metadata": {}, 499 | "outputs": [ 500 | { 501 | "name": "stdout", 502 | "output_type": "stream", 503 | "text": [ 504 | "[0.92430159+0.00000000e+00j 0.09827196+1.24080432e-01j\n", 505 | " 0.0090494 +3.44112962e-02j 0.09827196-1.24080432e-01j\n", 506 | " 0.06204022+1.73894604e-17j 0.01416663+4.55273048e-03j\n", 507 | " 0.0090494 -3.44112962e-02j 0.01416663-4.55273048e-03j\n", 508 | " 0.01365819+7.47203377e-18j]\n", 509 | "Type: \n", 510 | "Shape: (9,)\n" 511 | ] 512 | } 513 | ], 514 | "source": [ 515 | "result = SVD(coeffs)\n", 516 | "print(result)\n", 517 | "print('Type: {}'.format(type(result)))\n", 518 | "print('Shape: {}'.format(result.shape))" 519 | ] 520 | }, 521 | { 522 | "cell_type": "markdown", 523 | "metadata": {}, 524 | "source": [ 525 | "For ease of calculation we combine all of these functions into one function that takes lists of the different parameters ($\\Omega$, $\\Delta$, $\\Gamma$ etc.) and outputs the steady state solution for the density matrix in the order that `rho_list` is created. Note that it outputs the solution of the entire density matrix, so we need to extract the element of interest (usually $\\rho_{21}$). All we need to do now is pass the system parameters to this function. " 526 | ] 527 | }, 528 | { 529 | "cell_type": "code", 530 | "execution_count": 28, 531 | "metadata": {}, 532 | "outputs": [], 533 | "source": [ 534 | "def steady_state_soln(Omegas, Deltas, Gammas, gammas = []):\n", 535 | " L_atom = L_decay(Gammas)\n", 536 | " if len(gammas) != 0: \n", 537 | " L_laser = L_dephasing(gammas)\n", 538 | " L_tot = L_atom + L_laser\n", 539 | " else:\n", 540 | " L_tot = L_atom\n", 541 | " H = Hamiltonian(Omegas, Deltas)\n", 542 | " Master = Master_eqn(H, L_tot)\n", 543 | " rho_coeffs = OBE_matrix(Master)\n", 544 | " soln = SVD(rho_coeffs)\n", 545 | " return soln" 546 | ] 547 | }, 548 | { 549 | "cell_type": "code", 550 | "execution_count": 29, 551 | "metadata": {}, 552 | "outputs": [ 553 | { 554 | "name": "stdout", 555 | "output_type": "stream", 556 | "text": [ 557 | "[0.92430159+0.00000000e+00j 0.09827196+1.24080432e-01j\n", 558 | " 0.0090494 +3.44112962e-02j 0.09827196-1.24080432e-01j\n", 559 | " 0.06204022+1.73894604e-17j 0.01416663+4.55273048e-03j\n", 560 | " 0.0090494 -3.44112962e-02j 0.01416663-4.55273048e-03j\n", 561 | " 0.01365819+7.47203377e-18j]\n", 562 | "Type: \n", 563 | "Shape: (9,)\n" 564 | ] 565 | } 566 | ], 567 | "source": [ 568 | "also_result = steady_state_soln(Omegas, Deltas, Gammas, gammas)\n", 569 | "print(also_result)\n", 570 | "print('Type: {}'.format(type(also_result)))\n", 571 | "print('Shape: {}'.format(also_result.shape))" 572 | ] 573 | }, 574 | { 575 | "cell_type": "markdown", 576 | "metadata": {}, 577 | "source": [ 578 | "## Measurable Quantities\n", 579 | "\n", 580 | "We now have the values of the steady state density matrix elements, from which we need to extract useful measurable quantities to compare to experiment. Often the experimental quantity of interest is the probe transmission, where the probe is the laser coupling the lowest two levels. The absorption coefficient of the system $\\alpha$ can be found from the imaginary part of the complex susceptibility $\\chi$ through $\\alpha = k\\textrm{Im}{[\\chi]}$. The susceptibility of the medium to the probe is related to the steady state coherence term $\\rho_{21}$ through $$\\chi = -\\frac{2N|d_{12}|^2}{\\hbar \\epsilon_0 \\Omega_{p}}\\rho_{21}$$ where $N$ is the atomic number density and $d_{12}$ is the dipole matrix element of the $|1\\rangle \\rightarrow |2\\rangle$ transition. From these expressions it can be seen that the absorption of the probe $\\alpha_{12} \\propto -\\textrm{Im}[\\rho_{21}]$ and hence the probe transmission can be approximated as $1 + \\textrm{Im}[\\rho_{21}]$.\n", 581 | "\n", 582 | "Since $\\rho_{21} = \\rho_{12}^*$, we will usually extract $\\rho_{12}$ from the vector of the solution as this is always at the same index (`[1]`) whereas the position of $\\rho_{21}$ will vary depending on the number of levels. The probe absorption is then $\\alpha_{12} \\propto \\textrm{Im}[\\rho_{12}]$." 583 | ] 584 | }, 585 | { 586 | "cell_type": "markdown", 587 | "metadata": {}, 588 | "source": [ 589 | "## Analytic Solutions\n", 590 | "\n", 591 | "Obviously because every parameter step requires solving an eigenvalue problem, this method is NOT fast. However it does allow you to compute results for parameters outside of the weak probe regime (defined as $\\Omega_{12}^2/\\Gamma_3(\\Gamma_2 + \\Gamma_3)\\ll 1$). If your system is in the weak probe regime, there are explicit analytic solutions, especially for low numbers of levels (usually <5).\n" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": 30, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "def fast_3_level(Omegas, Deltas, Gammas, gammas = []):\n", 601 | " Delta_12, Delta_23 = Deltas[:]\n", 602 | " Omega_12, Omega_23 = Omegas[:]\n", 603 | " Gamma_2, Gamma_3 = Gammas[:]\n", 604 | " if len(gammas) != 0:\n", 605 | " gamma_12, gamma_23 = gammas[:]\n", 606 | " else:\n", 607 | " gamma_12, gamma_23 = 0, 0\n", 608 | " bracket_1 = -1j*(Delta_12 + Delta_23) + (Gamma_3/2) + gamma_12 + gamma_23\n", 609 | " bracket_2 = -1j*Delta_12 + (Gamma_2/2) + gamma_12 + (Omega_23**2)/(4*bracket_1)\n", 610 | " return -1j*Omega_12/(2*bracket_2)\n", 611 | "\n", 612 | "def fast_4_level(Omegas, Deltas, Gammas, gammas = []):\n", 613 | " Omega_12, Omega_23, Omega_34 = Omegas[:]\n", 614 | " Delta_12, Delta_23, Delta_34 = Deltas[:]\n", 615 | " Gamma_2, Gamma_3, Gamma_4 = Gammas[:]\n", 616 | " if len(gammas) != 0:\n", 617 | " gamma_12, gamma_23, gamma_34 = gammas[:]\n", 618 | " else:\n", 619 | " gamma_12, gamma_23, gamma_34 = 0,0,0\n", 620 | " bracket_1 = 1j*(Delta_12 + Delta_23 + Delta_34) - gamma_12 - gamma_23 - gamma_34 - (Gamma_4/2)\n", 621 | " bracket_2 = 1j*(Delta_12 + Delta_23) - (Gamma_3/2) - gamma_12 - gamma_23 + (Omega_34**2)/(4*bracket_1)\n", 622 | " bracket_3 = 1j*Delta_12 - (Gamma_2/2) - gamma_12 + (Omega_23**2)/(4*bracket_2)\n", 623 | " return (1j*Omega_12)/(2*bracket_3)" 624 | ] 625 | }, 626 | { 627 | "cell_type": "markdown", 628 | "metadata": {}, 629 | "source": [ 630 | "### Analytic solution for an arbitrary number of levels\n", 631 | "\n", 632 | "It is possible to formulate an expression for the analytic solution in the weak probe regime for a ladder scheme with any number of levels. This has to be done iteratively. \n", 633 | "\n", 634 | "For an system with $n$ levels (hence $n-1$ driving fields) we have an expression for the probe coherence which needs to be evaluated for all $m=1, 2 \\dots n-2, n-1$\n", 635 | "\n", 636 | "$$\\rho_{21} = \\frac{2iK_1}{\\Omega_{12}} $$\n", 637 | "where\n", 638 | "$$K_m = \\left(\\frac{\\Omega_{m, m+1}}{2}\\right)^2\\left[Z_m + K_{m+1}\\right]^{-1}$$\n", 639 | "and $$Z_m = \\sum_{k=1}^m \\left(i\\Delta_{k, k+1} - \\gamma_{k, k+1}\\right) -\\frac{\\Gamma_{m+1}}{2}.$$\n", 640 | "\n", 641 | "For example, if we have a 3-level scheme with two driving fields 1 and 2, then the expression will look like this\n", 642 | "\n", 643 | "$$\\rho_{21} = \\frac{2i}{\\Omega_{12}}\\left(\\frac{\\Omega_{12}}{2}\\right)^2\\left[Z_1 + \\left(\\frac{\\Omega_{23}}{2}\\right)^2\\left[Z_2\\right]^{-1}\\right]^{-1}$$\n", 644 | "where\n", 645 | "$$ Z_1 = i\\Delta_{12} - \\gamma_{12} - \\Gamma_2/2$$\n", 646 | "and\n", 647 | "$$ Z_2 = i(\\Delta_{12} + \\Delta_{23}) - (\\gamma_{12} + \\gamma_{23}) - \\Gamma_3/2$$" 648 | ] 649 | }, 650 | { 651 | "cell_type": "markdown", 652 | "metadata": {}, 653 | "source": [ 654 | "This has been encapsulated into the function `fast_n_level` which takes the same sets of parameters as the `steady_state_soln` matrix method and returns the value of $\\rho_{21}$." 655 | ] 656 | }, 657 | { 658 | "cell_type": "code", 659 | "execution_count": 31, 660 | "metadata": {}, 661 | "outputs": [], 662 | "source": [ 663 | "def term_n(n, Deltas, Gammas, gammas = []):\n", 664 | " if len(gammas) == 0:\n", 665 | " gammas = numpy.zeros((len(Deltas)))\n", 666 | " # n>0\n", 667 | " return 1j*(numpy.sum(Deltas[:n+1])) - (Gammas[n]/2) - numpy.sum(gammas[:n+1])\n", 668 | "\n", 669 | "def fast_n_level(Omegas, Deltas, Gammas, gammas = []):\n", 670 | " n_terms = len(Omegas)\n", 671 | " term_counter = numpy.arange(n_terms)[::-1]\n", 672 | " func = 0\n", 673 | " for n in term_counter:\n", 674 | " prefact = (Omegas[n]/2)**2\n", 675 | " term = term_n(n, Deltas, Gammas, gammas)\n", 676 | " func = prefact/(term+func)\n", 677 | " return (2j/Omegas[0])*func" 678 | ] 679 | }, 680 | { 681 | "cell_type": "code", 682 | "execution_count": 32, 683 | "metadata": {}, 684 | "outputs": [ 685 | { 686 | "name": "stdout", 687 | "output_type": "stream", 688 | "text": [ 689 | "(0.008606577250114471-0.006320645282155274j)\n", 690 | "Type: \n", 691 | "(0.008606577250114473-0.0063206452821552754j)\n", 692 | "Type: \n" 693 | ] 694 | } 695 | ], 696 | "source": [ 697 | "Omegas = [0.1,5]\n", 698 | "Deltas = [5,0]\n", 699 | "Gammas = [5,1]\n", 700 | "gammas = [0.1,0.1]\n", 701 | "\n", 702 | "explicit = fast_3_level(Omegas, Deltas, Gammas, gammas)\n", 703 | "iterative = fast_n_level(Omegas, Deltas, Gammas, gammas)\n", 704 | "\n", 705 | "print(explicit)\n", 706 | "print('Type: {}'.format(type(explicit)))\n", 707 | "print(iterative)\n", 708 | "print('Type: {}'.format(type(iterative)))" 709 | ] 710 | }, 711 | { 712 | "cell_type": "markdown", 713 | "metadata": {}, 714 | "source": [ 715 | "## Thermal Ensembles - Doppler Averaging\n", 716 | "\n", 717 | "Atoms in a thermal ensemble see frequency-shifted fields due to the Doppler effect. For now, we will only consider the 1D case, where all beams are either co- or counter-propagating. An atom moving in the same direction as the propagation of a laser field will see a red-shifted frequency, whereas an atom moving in the opposite direction would see a blue-shifted frequency. This will modify the detunings in the Hamiltonian as $\\Delta_{\\rm{eff}} = \\Delta - \\vec{k}\\cdot \\vec{v}$ where $\\vec{k}$ is the wavevector of the laser light and $\\vec{v}$ is the atomic velocity. Hence for an atom moving in the propagation direction we have $\\Delta_{\\rm{eff}} = \\Delta- kv$ and for an atom moving opposite to the direction of propagation we get $\\Delta_{\\rm{eff}} = \\Delta + kv$. The actual value of the sign does not matter (as the velocity distribution will be symmetric about 0), as long as we are consistent with the signs of the wavevectors relative to each other.\n", 718 | "\n", 719 | "### Distribution of Velocities\n", 720 | "\n", 721 | "The atoms will have a distribution of velocities as given by the Maxwell-Boltzmann distribution\n", 722 | "\n", 723 | "$$f_{v}(v_i) = \\sqrt\\frac{m}{2\\pi k_B T} \\exp \\left(-\\frac{mv_{i}^2}{2k_BT}\\right) $$\n", 724 | "\n", 725 | "where $m$ is the mass of an atom, $k_B$ is the Boltzmann contant and $T$ is the temperature of the vapour in Kelvin. We can either define this function explicitly, or recognise that this is a Gaussian distribution centred on 0 with width $\\sqrt{\\frac{k_BT}{m}}$ and use the built-in `scipy.stats.norm` functions." 726 | ] 727 | }, 728 | { 729 | "cell_type": "code", 730 | "execution_count": 58, 731 | "metadata": {}, 732 | "outputs": [], 733 | "source": [ 734 | "def MBdist(velocities, temp = 20, mass = 132.90545):\n", 735 | " m = constants.m_u*mass #kg\n", 736 | " kb = constants.k #m**2 kg s**-2 K**-1\n", 737 | " temp_k = temp + 273.15 #K\n", 738 | " sigma = numpy.sqrt((kb*temp_k)/m)\n", 739 | " return stats.norm(0, sigma).pdf(velocities)\n", 740 | "\n", 741 | "def MBdist2(velocities, temp = 20, mass = 132.90545):\n", 742 | " kb = 1.38e-23 #m**2 kg s**-2 K**-1\n", 743 | " m = mass*1.66e-27 #kg\n", 744 | " T = temp+273.15 #Kelvin\n", 745 | " f = numpy.sqrt(m/(2*numpy.pi*kb*T))*numpy.exp((-m*velocities**2)/(2*kb*T))\n", 746 | " return f" 747 | ] 748 | }, 749 | { 750 | "cell_type": "code", 751 | "execution_count": 60, 752 | "metadata": {}, 753 | "outputs": [ 754 | { 755 | "name": "stdout", 756 | "output_type": "stream", 757 | "text": [ 758 | "[3.22904507e-06 4.76995444e-05 3.59413448e-04 1.38138388e-03\n", 759 | " 2.70816060e-03 2.70816060e-03 1.38138388e-03 3.59413448e-04\n", 760 | " 4.76995444e-05 3.22904507e-06]\n", 761 | "Type: \n", 762 | "Shape: (10,)\n" 763 | ] 764 | } 765 | ], 766 | "source": [ 767 | "vs = numpy.linspace(-500,500,10)\n", 768 | "\n", 769 | "MB1 = MBdist(vs)\n", 770 | "\n", 771 | "print(MB1)\n", 772 | "print('Type: {}'.format(type(MB1)))\n", 773 | "print('Shape: {}'.format(MB1.shape))" 774 | ] 775 | }, 776 | { 777 | "cell_type": "markdown", 778 | "metadata": {}, 779 | "source": [ 780 | "### Averaging over velocity classes\n", 781 | "\n", 782 | "Once we have calculated the response of the vapour (usually the probe coherence) for each velocity class, we need to integrate. We can either do the integration manually (using the rectangle rule for example) or using a built-in routine (such as `scipy.integrate.simpson`). In the case of manual integration, for each value of detuning we multiply the probe coherence for each velocity class by its relative abundance as given by the Maxwell-Boltzmann distribution. This abundance can be calculated using the `MBdist` (or `MBdist2`) functions we defined above. Then we multiply by the width of the velocity class, and sum over all velocities." 783 | ] 784 | }, 785 | { 786 | "cell_type": "markdown", 787 | "metadata": {}, 788 | "source": [ 789 | "# Time-Dependent Solutions\n", 790 | "\n", 791 | "If we are not considering the steady state, we need to consider the time evolution of either the state vector $|\\psi\\rangle$ or the density matrix components $\\rho_{\\rm{vect}}$.\n", 792 | "\n", 793 | "First let us consider the state vector for a two-level system $|\\psi(t)\\rangle = c_1(t)|1\\rangle + c_2(t)|2\\rangle$ which can be written as\n", 794 | "$$|\\psi(t)\\rangle = \\begin{pmatrix} c_1(t) \\\\ c_2(t) \\end{pmatrix}.$$\n", 795 | "\n", 796 | "The time evolution of the state vector is of course governed by the Schrödinger equation\n", 797 | "$$i\\hbar\\frac{\\partial |\\psi\\rangle}{\\partial t} = \\hat{H}|\\psi\\rangle.$$\n", 798 | "\n", 799 | "The state at a time $t$ is then given by\n", 800 | "$$|\\psi(t)\\rangle = \\exp\\left(-i\\hat{H}t\\right) |\\psi(0)\\rangle$$\n", 801 | "where $\\hat{H}$ is the time-independent Hamiltonian describing the evolution of the state vector, and $|\\psi(0)\\rangle$ is the state vector at time $t=0$. (We have set $\\hbar=1$ for simplicity).\n", 802 | "\n", 803 | "For a two-level system, the interaction Hamiltonian can be written\n", 804 | "$$ \\hat{H} = \\frac{\\hbar}{2}\\begin{pmatrix} 0 & \\Omega \\\\ \\Omega & -2\\Delta \\end{pmatrix}.$$\n", 805 | "\n", 806 | "We can use the `Hamiltonian` function to construct the Hamiltonian for an n-level system by supplying lists of the Rabi frequencies and detunings.\n", 807 | "\n", 808 | "We can then define a function for the time-dependent part of the equation. In the above example the term `operator` is equal to $-i\\hat{H}$ but we will see later that this is not always the case." 809 | ] 810 | }, 811 | { 812 | "cell_type": "code", 813 | "execution_count": 64, 814 | "metadata": {}, 815 | "outputs": [], 816 | "source": [ 817 | "def time_evolve(operator, t, p_0):\n", 818 | " exponent = operator*t\n", 819 | " term = linalg.expm(exponent) #linalg.expm does matrix exponentiation\n", 820 | " return numpy.matmul(term, p_0)" 821 | ] 822 | }, 823 | { 824 | "cell_type": "markdown", 825 | "metadata": {}, 826 | "source": [ 827 | "Often, at $t=0$ all population is in the ground state, so \n", 828 | "$$|\\psi(t)\\rangle = \\begin{pmatrix} 1 \\\\ 0 \\end{pmatrix}.$$" 829 | ] 830 | }, 831 | { 832 | "cell_type": "code", 833 | "execution_count": 65, 834 | "metadata": {}, 835 | "outputs": [ 836 | { 837 | "name": "stdout", 838 | "output_type": "stream", 839 | "text": [ 840 | "[0.+0.j 0.-1.j]\n", 841 | "Type: \n", 842 | "Shape: (2,)\n" 843 | ] 844 | } 845 | ], 846 | "source": [ 847 | "Omegas = [1] # 2pi MHz\n", 848 | "Deltas = [0] # 2pi MHz\n", 849 | "H = Hamiltonian(Omegas, Deltas) #Create Hamiltonian\n", 850 | "psi_0 = numpy.transpose(numpy.asarray([1,0])) #state vector at t=0\n", 851 | "t_pi = Omegas[0]*numpy.pi\n", 852 | "\n", 853 | "sol_pi = time_evolve(-1j*H, t_pi, psi_0)\n", 854 | "\n", 855 | "print(sol_pi)\n", 856 | "print('Type: {}'.format(type(sol_pi)))\n", 857 | "print('Shape: {}'.format(sol_pi.shape))" 858 | ] 859 | }, 860 | { 861 | "cell_type": "markdown", 862 | "metadata": {}, 863 | "source": [ 864 | "Considering only the state vector, it is not possible to consider the effects of decay. To do this we need to consider the density matrix $\\rho$ and its time evolution. \n", 865 | "\n", 866 | "The time evolution of the density matrix is governed by the Lindblad Master equation which can be written\n", 867 | "\n", 868 | "$$\\frac{\\partial \\rho}{\\partial t} = -\\frac{i}{\\hbar}\\left[\\hat{H}, \\rho\\right] + \\mathcal{L}(\\rho)$$ where $\\hat{H}$ is the total Hamiltonian in the interaction picture and $\\mathcal{L}(\\rho)$ is an operator describing decay and dephasing within the system.\n", 869 | "We can rewrite this in terms of a vector $\\rho_{\\rm{vect}}$ that contains all the elements of the density matrix, and a matrix of time-independent coefficients $\\hat{M}$ found from the Master equation as\n", 870 | "$$\\frac{\\partial \\rho_{\\rm{vect}}}{\\partial t} = \\hat{M} \\rho_{\\rm{vect}}.$$\n", 871 | "\n", 872 | "We can use some of the previously defined functions to construct the operator $\\hat{M}$.\n", 873 | "\n", 874 | "Then we can find the values of $\\rho_{\\rm{vect}}$ at any time $t$ through\n", 875 | "$$\\rho_{\\rm{vect}}(t) = \\exp\\left(\\hat{M}t\\right)\\rho_{\\rm{vect}}(0)$$\n", 876 | "where $\\rho_{\\rm{vect}}(0)$ is the value of the density matrix in vector form at $t=0$.\n" 877 | ] 878 | }, 879 | { 880 | "cell_type": "code", 881 | "execution_count": 48, 882 | "metadata": {}, 883 | "outputs": [], 884 | "source": [ 885 | "def time_dep_matrix(Omegas, Deltas, Gammas, gammas = []):\n", 886 | " # first create decay/dephasing operators\n", 887 | " L_atom = L_decay(Gammas)\n", 888 | " if len(gammas) != 0: \n", 889 | " L_laser = L_dephasing(gammas)\n", 890 | " L_tot = L_atom + L_laser\n", 891 | " else:\n", 892 | " L_tot = L_atom\n", 893 | " H = Hamiltonian(Omegas, Deltas) #create the total Hamiltonian\n", 894 | " Master = Master_eqn(H, L_tot) #put Hamiltonian and decay/dephasing operators into Master equation\n", 895 | " rho_coeffs = OBE_matrix(Master) #create matrix of coefficients\n", 896 | " return rho_coeffs" 897 | ] 898 | }, 899 | { 900 | "cell_type": "code", 901 | "execution_count": 62, 902 | "metadata": {}, 903 | "outputs": [ 904 | { 905 | "name": "stdout", 906 | "output_type": "stream", 907 | "text": [ 908 | "[[ 0. +0.j 0. +0.05j 0. +0.j 0. -0.05j 5. +0.j 0. +0.j\n", 909 | " 0. +0.j 0. +0.j 0. +0.j ]\n", 910 | " [ 0. +0.05j -2.6-5.j 0. +2.5j 0. +0.j 0. -0.05j 0. +0.j\n", 911 | " 0. +0.j 0. +0.j 0. +0.j ]\n", 912 | " [ 0. +0.j 0. +2.5j -0.7-5.j 0. +0.j 0. +0.j 0. -0.05j\n", 913 | " 0. +0.j 0. +0.j 0. +0.j ]\n", 914 | " [ 0. -0.05j 0. +0.j 0. +0.j -2.6+5.j 0. +0.05j 0. +0.j\n", 915 | " 0. -2.5j 0. +0.j 0. +0.j ]\n", 916 | " [ 0. +0.j 0. -0.05j 0. +0.j 0. +0.05j -5. +0.j 0. +2.5j\n", 917 | " 0. +0.j 0. -2.5j 1. +0.j ]\n", 918 | " [ 0. +0.j 0. +0.j 0. -0.05j 0. +0.j 0. +2.5j -3.1+0.j\n", 919 | " 0. +0.j 0. +0.j 0. -2.5j ]\n", 920 | " [ 0. +0.j 0. +0.j 0. +0.j 0. -2.5j 0. +0.j 0. +0.j\n", 921 | " -0.7+5.j 0. +0.05j 0. +0.j ]\n", 922 | " [ 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. -2.5j 0. +0.j\n", 923 | " 0. +0.05j -3.1+0.j 0. +2.5j ]\n", 924 | " [ 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. +0.j 0. -2.5j\n", 925 | " 0. +0.j 0. +2.5j -1. +0.j ]]\n", 926 | "Type: \n", 927 | "Shape: (9, 9)\n" 928 | ] 929 | } 930 | ], 931 | "source": [ 932 | "Omegas = [0.1,5]\n", 933 | "Deltas = [5,0]\n", 934 | "Gammas = [5,1]\n", 935 | "gammas = [0.1,0.1]\n", 936 | "M = time_dep_matrix(Omegas, Deltas, Gammas, gammas)\n", 937 | "print(M)\n", 938 | "print('Type: {}'.format(type(M)))\n", 939 | "print('Shape: {}'.format(M.shape))" 940 | ] 941 | }, 942 | { 943 | "cell_type": "code", 944 | "execution_count": null, 945 | "metadata": {}, 946 | "outputs": [], 947 | "source": [] 948 | } 949 | ], 950 | "metadata": { 951 | "kernelspec": { 952 | "display_name": "Python 3", 953 | "language": "python", 954 | "name": "python3" 955 | }, 956 | "language_info": { 957 | "codemirror_mode": { 958 | "name": "ipython", 959 | "version": 3 960 | }, 961 | "file_extension": ".py", 962 | "mimetype": "text/x-python", 963 | "name": "python", 964 | "nbconvert_exporter": "python", 965 | "pygments_lexer": "ipython3", 966 | "version": "3.8.8" 967 | } 968 | }, 969 | "nbformat": 4, 970 | "nbformat_minor": 2 971 | } 972 | -------------------------------------------------------------------------------- /OBE_Tools.py: -------------------------------------------------------------------------------- 1 | '''Series of functions to be used to solve the Optical Bloch equations for 2 | atomic systems in a ladder configuration with any number of levels. 3 | For more details and examples of use see the accompanying Jupyter Notebooks 4 | 'OBE Functions' and 'Examples' available on GitHub 5 | (https://github.com/LucyDownes/OBE_Python_Tools). 6 | 7 | Author: Lucy Downes (lucy.downes@durham.ac.uk) 8 | Date: 12/07/2023 9 | 10 | Requires: 11 | Numpy, Scipy (constants, stats, linalg) and Sympy. 12 | ''' 13 | 14 | import numpy 15 | import sympy 16 | import scipy.constants as const 17 | import scipy.stats as stats 18 | import scipy.linalg as linalg 19 | 20 | def create_rho_list(levels = 3): 21 | """ 22 | Create a list of Sympy symbolic objects to represent the elements of the 23 | density matrix (rho_ij) for a given number of levels 24 | 25 | Args: 26 | levels (int, optional): Number of levels in the atomic scheme. 27 | Defaults to 3. 28 | 29 | Returns: 30 | List: list containing Sympy symbolic objects. 31 | Length n^2 for n atomic levels. 32 | """ 33 | rho_list = [] 34 | for i in range(levels): 35 | for j in range(levels): 36 | globals()['rho_'+str(i+1)+str(j+1)] = sympy.Symbol( 37 | 'rho_'+str(i+1)+str(j+1)) 38 | rho_list.append(globals()['rho_'+str(i+1)+str(j+1)]) 39 | return rho_list 40 | 41 | def create_rho_matrix(levels = 3): 42 | """Create density matrix where elements are Sympy symbolic objects. 43 | 44 | Args: 45 | levels (int, optional): Number of levels in the atomic scheme. 46 | Defaults to 3. 47 | 48 | Returns: 49 | numpy.matrix: matrix of Sympy symbolic objects. 50 | Matrix size will be nxn for an n-level system. 51 | """ 52 | rho_matrix = numpy.empty((levels, levels), dtype = 'object') 53 | for i in range(levels): 54 | for j in range(levels): 55 | globals()['rho_'+str(i+1)+str(j+1)] = sympy.Symbol( 56 | 'rho_'+str(i+1)+str(j+1)) 57 | rho_matrix[i,j] = globals()['rho_'+str(i+1)+str(j+1)] 58 | return numpy.matrix(rho_matrix) 59 | 60 | def Hamiltonian(Omegas, Deltas): 61 | """ 62 | Given lists of Rabi frequencies and detunings, construct interaction 63 | Hamiltonian (assuming RWA and dipole approximation) as per equation 14. 64 | h_bar = 1 for simplicity. 65 | Both lists should be in ascending order (Omega_12, Omega_23 etc) 66 | 67 | Args: 68 | Omegas (list of floats): List of Rabi frequencies for all fields 69 | (n-1 fields for n atomic levels) 70 | Deltas (list of floats): List of detunings for all fields 71 | (n-1 fields for n atomic levels) 72 | 73 | Returns: 74 | numpy.matrix: the total interaction Hamiltonian in matrix form. 75 | Will be of shape nxn for an n-level system. 76 | """ 77 | levels = len(Omegas)+1 78 | H = numpy.zeros((levels,levels)) 79 | for i in range(levels): 80 | for j in range(levels): 81 | if numpy.logical_and(i==j, i!=0): 82 | H[i,j] = -2*(numpy.sum(Deltas[:i])) 83 | elif numpy.abs(i-j) == 1: 84 | H[i,j] = Omegas[numpy.min([i,j])] 85 | return numpy.matrix(H/2) 86 | 87 | def L_decay(Gammas): 88 | """ 89 | Given a list of linewidths for each atomic level, 90 | construct atomic decay operator in matrix form for a ladder system. 91 | Assumes that there is no decay from the lowest level (\Gamma_1 = 0). 92 | 93 | Args: 94 | Gammas (list of floats): Linewidth of the atomic states considered. 95 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 96 | Values should be given in order of lowest to highest energy level 97 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 98 | n-1 values for an n-level system. 99 | 100 | Returns: 101 | numpy.matrix: Decay operator as a matrix containing multiples of Sympy 102 | symbolic objects. Will be of size nxn for an n-level system. 103 | """ 104 | levels = len(Gammas)+1 105 | rhos = create_rho_matrix(levels = levels) 106 | Gammas_all = [0] + Gammas 107 | decay_matrix = numpy.zeros((levels, levels), dtype = 'object') 108 | for i in range(levels): 109 | for j in range(levels): 110 | if i != j: 111 | decay_matrix[i,j] = -0.5*( 112 | Gammas_all[i]+Gammas_all[j])*rhos[i,j] 113 | elif i != levels - 1: 114 | into = Gammas_all[i+1]*rhos[1+i, j+1] 115 | outof = Gammas_all[i]*rhos[i, j] 116 | decay_matrix[i,j] = into - outof 117 | else: 118 | outof = Gammas_all[i]*rhos[i, j] 119 | decay_matrix[i,j] = - outof 120 | return numpy.matrix(decay_matrix) 121 | 122 | def L_dephasing(gammas): 123 | """ 124 | Given list of laser linewidths, create dephasing operator in matrix form. 125 | 126 | Args: 127 | gammas (list): Linewidths of the field coupling each pair of states 128 | in the ladder, from lowest to highest energy states 129 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 130 | 131 | Returns: 132 | numpy.matrix: Dephasing operator as a matrix populated by expressions 133 | containing Sympy symbolic objects. 134 | Will be size nxn for an n-level system. 135 | """ 136 | levels = len(gammas)+1 137 | rhos = create_rho_matrix(levels = levels) 138 | deph_matrix = numpy.zeros((levels, levels), dtype = 'object') 139 | for i in range(levels): 140 | for j in range(levels): 141 | if i != j: 142 | deph_matrix[i,j] = -(numpy.sum(gammas[numpy.min( 143 | [i,j]):numpy.max([i,j])]))*rhos[i,j] 144 | return numpy.matrix(deph_matrix) 145 | 146 | def Master_eqn(H_tot, L): 147 | """ 148 | Return an expression for the right hand side of the Master equation 149 | (as in Eqn 18) for a given Hamiltonian and Lindblad term. 150 | Assumes that the number of atomic levels is set by the shape of the 151 | Hamiltonian matrix (nxn for n levels). 152 | 153 | Args: 154 | H_tot (matrix): The total Hamiltonian in the interaction picture. 155 | L (matrix): The Lindblad superoperator in matrix form. 156 | 157 | Returns: 158 | numpy.matrix: Right hand side of the Master equation (eqn 18) 159 | as a matrix containing expressions in terms of Sympy symbolic objects. 160 | Will be size nxn for an n-level system. 161 | """ 162 | levels = H_tot.shape[0] 163 | dens_mat = create_rho_matrix(levels = levels) 164 | return -1j*(H_tot*dens_mat - dens_mat*H_tot) + L 165 | 166 | def OBE_matrix(Master_matrix): 167 | """ 168 | Take the right hand side of the Master equation (-i[H,\rho] + L) 169 | expressed as an array of multiples of Sympy symbolic objects and output 170 | an ndarray of coefficients M such that d rho_vect/dt = M*rho_vect 171 | where rho_vect is the vector of density matrix elements. 172 | 173 | Args: 174 | Master_matrix (matrix): The right hand side of the Master equation 175 | as a matrix of multiples of Sympy symbolic objects 176 | 177 | Returns: 178 | numpy.ndarray: An array of (complex) coefficients. 179 | Will be size n^2 x n^2 for an n-level system. 180 | """ 181 | levels = Master_matrix.shape[0] 182 | rho_vector = create_rho_list(levels = levels) 183 | coeff_matrix = numpy.zeros((levels**2, levels**2), dtype = 'complex') 184 | count = 0 185 | for i in range(levels): 186 | for j in range(levels): 187 | entry = Master_matrix[i,j] 188 | expanded = sympy.expand(entry) 189 | #use Sympy coeff to extract coefficient of each element in rho_vect 190 | for n,r in enumerate(rho_vector): 191 | coeff_matrix[count, n] = complex(expanded.coeff(r)) 192 | count += 1 193 | return coeff_matrix 194 | 195 | def SVD(coeff_matrix): 196 | """ 197 | Perform singular value decomposition (SVD) on matrix of coefficients 198 | using the numpy.linalg.svd function. 199 | SVD returns US(V)*^T where S is the diagonal matrix of singular values. 200 | The solution of the system of equations is then the column of V 201 | corresponding to the zero singular value. 202 | If there is no zero singular value (within tolerance, allowing for 203 | floating point precision) then return a zero array. 204 | 205 | Args: 206 | coeff_matrix (ndarray): array of complex coefficients M that satisfies 207 | the expression d rho_vect/dt = M*rho_vect where rho_vect is the 208 | vector of density matrix elements. 209 | 210 | Returns: 211 | ndarray: 1D array of complex floats corresponding to the steady-state 212 | value of each element of the density matrix in the order given by 213 | the rho_list function. Will be length n for an n-level system. 214 | """ 215 | levels = int(numpy.sqrt(coeff_matrix.shape[0])) 216 | u,sig,v = numpy.linalg.svd(coeff_matrix) 217 | abs_sig = numpy.abs(sig) 218 | minval = numpy.min(abs_sig) 219 | if minval>1e-12: 220 | print('ERROR - Matrix is non-singular') 221 | return numpy.zeros((levels**2)) 222 | index = abs_sig.argmin() 223 | rho = numpy.conjugate(v[index,:]) 224 | #SVD returns the conjugate transpose of v 225 | pops = numpy.zeros((levels)) 226 | for l in range(levels): 227 | pops[l] = (numpy.real(rho[(l*levels)+l])) 228 | t = 1/(numpy.sum(pops)) #Normalise so the sum of the populations is one 229 | rho_norm = rho*t.item() 230 | return rho_norm 231 | 232 | 233 | 234 | def steady_state_soln(Omegas, Deltas, Gammas, gammas = []): 235 | """ 236 | Given lists of parameters (all in order of lowers to highest energy level), 237 | construct and solve for steady state of density matrix. 238 | Returns ALL elements of density matrix in order of rho_list 239 | (rho_11, rho_12, ... rho_i1, rho_i2, ..., \rho_{n, n-1}, rho_nn). 240 | 241 | Args: 242 | Omegas (list of floats): Rabi frequencies of fields coupling each pair 243 | of states in the ladder, from lowest to highest energy states 244 | (\Omega_{12}, ..., \Omega{n-1,n} for n levels). 245 | Deltas (list of floats): Detuning of fields coupling each pair 246 | of states in the ladder, from lowest to highest energy states 247 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 248 | Gammas (list of floats): Linewidth of the atomic states considered. 249 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 250 | Values should be given in order of lowest to highest energy level 251 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 252 | n-1 values for n levels. 253 | gammas (list of floats, optional): Linewidths of the fields coupling 254 | each pair of states in the ladder, from lowest to highest energy 255 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 256 | Defaults to [] (meaning all \gamma_ij = 0). 257 | 258 | Returns: 259 | ndarray: 1D array containing values for each element of the density 260 | matrix in the steady state, in the order returned by the rho_list 261 | function (\rho_11, \rho_12, ..., \rho_{n, n-1}, \rho_nn). 262 | Will be length n for an n-level system. 263 | """ 264 | L_atom = L_decay(Gammas) 265 | if len(gammas) != 0: 266 | L_laser = L_dephasing(gammas) 267 | L_tot = L_atom + L_laser 268 | else: 269 | L_tot = L_atom 270 | H = Hamiltonian(Omegas, Deltas) 271 | Master = Master_eqn(H, L_tot) 272 | rho_coeffs = OBE_matrix(Master) 273 | soln = SVD(rho_coeffs) 274 | return soln 275 | 276 | def fast_3_level(Omegas, Deltas, Gammas, gammas = []): 277 | """ 278 | Calculate the analytic solution of the steady-state probe coherence 279 | (\rho_{21}) in the weak probe limit for a 3-level ladder system. 280 | 281 | Args: 282 | Omegas (list of floats): Rabi frequencies of fields coupling each pair 283 | of states in the ladder, from lowest to highest energy states 284 | (\Omega_{12}, ..., \Omega{n-1,n} for n levels). 285 | Deltas (list of floats): Detuning of fields coupling each pair 286 | of states in the ladder, from lowest to highest energy states 287 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 288 | Gammas (list of floats): Linewidth of the atomic states considered. 289 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 290 | Values should be given in order of lowest to highest energy level 291 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 292 | n-1 values for n levels. 293 | gammas (list of floats, optional): Linewidths of the field coupling 294 | each pair of states in the ladder, from lowest to highest energy 295 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 296 | Defaults to [] (meaning all \gamma_ij = 0). 297 | 298 | Returns: 299 | complex: steady-state value of the probe coherence (\rho_{21}) 300 | """ 301 | Delta_12, Delta_23 = Deltas[:] 302 | Omega_12, Omega_23 = Omegas[:] 303 | Gamma_2, Gamma_3 = Gammas[:] 304 | if len(gammas) != 0: 305 | gamma_12, gamma_23 = gammas[:] 306 | else: 307 | gamma_12, gamma_23 = 0, 0 308 | expression = (Omega_23**2/4)/(1j*(Delta_12 + Delta_23) + (Gamma_3/2) \ 309 | + gamma_12 + gamma_23) 310 | bottom = (1j*Delta_12) + (Gamma_2/2) + gamma_12 + expression 311 | rho = (1j*Omega_12)/(2*bottom) 312 | return numpy.conjugate(rho) 313 | 314 | def fast_4_level(Omegas, Deltas, Gammas, gammas = []): 315 | """ 316 | Analytic solution of the steady-state probe coherence (\rho_{21}) 317 | in the weak probe limit for a 4-level ladder system. 318 | 319 | Args: 320 | Omegas (list of floats): Rabi frequencies of fields coupling each pair 321 | of states in the ladder, from lowest to highest energy states 322 | (\Omega_{12}, ..., \Omega{n-1,n} for n levels). 323 | Deltas (list of floats): Detuning of fields coupling each pair 324 | of states in the ladder, from lowest to highest energy states 325 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 326 | Gammas (list of floats): Linewidth of the atomic states considered. 327 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 328 | Values should be given in order of lowest to highest energy level 329 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 330 | n-1 values for n levels. 331 | gammas (list of floats, optional): Linewidths of the field coupling 332 | each pair of states in the ladder, from lowest to highest energy 333 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 334 | Defaults to [] (meaning all \gamma_ij = 0). 335 | 336 | Returns: 337 | complex: steady-state value of the probe coherence (\rho_{21}) 338 | """ 339 | Omega_12, Omega_23, Omega_34 = Omegas[:] 340 | Delta_12, Delta_23, Delta_34 = Deltas[:] 341 | Gamma_2, Gamma_3, Gamma_4 = Gammas[:] 342 | if len(gammas) != 0: 343 | gamma_12, gamma_23, gamma_34 = gammas[:] 344 | else: 345 | gamma_12, gamma_23, gamma_34 = 0,0,0 346 | bracket_1 = 1j*(Delta_12 + Delta_23 + Delta_34) - gamma_12 - gamma_23 - \ 347 | gamma_34 - (Gamma_4/2) 348 | bracket_2 = 1j*(Delta_12 + Delta_23) - (Gamma_3/2) - gamma_12 - gamma_23 +\ 349 | (Omega_34**2)/(4*bracket_1) 350 | bracket_3 = 1j*Delta_12 - (Gamma_2/2) - gamma_12 + ( 351 | Omega_23**2)/(4*bracket_2) 352 | return (1j*Omega_12)/(2*bracket_3) 353 | 354 | def term_n(n, Deltas, Gammas, gammas = []): 355 | """ 356 | Generate the nth term in the iterative expansion method for calculating 357 | the probe coherence (\rho_{21}) for an arbitrary number of levels in the 358 | weak-probe limit. 359 | 360 | Args: 361 | n (int): Index (for n>0) 362 | Deltas (list of floats): Detuning of fields coupling each pair 363 | of states in the ladder, from lowest to highest energy states 364 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 365 | Gammas (list of floats): Linewidth of the atomic states considered. 366 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 367 | Values should be given in order of lowest to highest energy level 368 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 369 | n-1 values for n levels. 370 | gammas (list of floats, optional): Linewidths of the fields coupling 371 | each pair of states in the ladder, from lowest to highest energy 372 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 373 | Defaults to [] (meaning all \gamma_ij = 0). 374 | 375 | Returns: 376 | complex float: value of the probe coherence (\rho_{21}) 377 | in the steady-state 378 | """ 379 | if len(gammas) == 0: 380 | gammas = numpy.zeros((len(Deltas))) 381 | # n>0 382 | return 1j*(numpy.sum(Deltas[:n+1])) - (Gammas[n]/2) - numpy.sum( 383 | gammas[:n+1]) 384 | 385 | def fast_n_level(Omegas, Deltas, Gammas, gammas = []): 386 | """ 387 | Return the steady-state probe coherence (\rho_{21}) in the weak-probe limit 388 | for a ladder system with an arbitrary number of levels. 389 | Calls the term_n function. 390 | 391 | Args: 392 | Omegas (list of floats): Rabi frequencies of fields coupling each pair 393 | of states in the ladder, from lowest to highest energy states 394 | (\Omega_{12}, ..., \Omega{n-1,n} for n levels). 395 | Deltas (list of floats): Detuning of fields coupling each pair 396 | of states in the ladder, from lowest to highest energy states 397 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 398 | Gammas (list of floats): Linewidth of the atomic states considered. 399 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 400 | Values should be given in order of lowest to highest energy level 401 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 402 | n-1 values for n levels. 403 | gammas (list of floats, optional): Linewidths of the fields coupling 404 | each pair of states in the ladder, from lowest to highest energy 405 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 406 | Defaults to [] (meaning all \gamma_ij = 0). 407 | 408 | Returns: 409 | complex float: value of the probe coherence (\rho_{21}) 410 | in the steady-state 411 | """ 412 | if len(gammas) == 0: 413 | gammas = numpy.zeros((len(Omegas))) 414 | n_terms = len(Omegas) 415 | term_counter = numpy.arange(n_terms)[::-1] 416 | func = 0 417 | for n in term_counter: 418 | prefact = (Omegas[n]/2)**2 419 | term = term_n(n, Deltas, Gammas, gammas) 420 | func = prefact/(term+func) 421 | return (2j/Omegas[0])*func 422 | 423 | def MBdist(velocities, temp = 20, mass = 132.90545): 424 | """ 425 | Method for calculating the relative abundances of given velocity classes 426 | assuming a Maxwell-Boltzmann distribution of velocities for a given 427 | temperature and atomic mass using built-in Scipy methods. 428 | Defaults to Caesium (the best atom) at room temperature (20 degrees C). 429 | 430 | Args: 431 | velocities (1D array): atomic velocities in units of m/s 432 | temp (int, optional): Temperature of the atomic ensemble 433 | in degrees Celsius. Defaults to 20 deg C (room temp). 434 | mass (float, optional): Mass number of the atomic species used 435 | in atomic mass units. Defaults to 132.90545 amu (Caesium). 436 | 437 | Returns: 438 | numpy.ndarray: relative abundances of the specified velocities 439 | according to the Maxwell-Boltzmann distribution. 440 | Will be the same shape as velocities. 441 | """ 442 | m = const.m_u*mass #kg 443 | kb = const.k #m**2 kg s**-2 K**-1 444 | temp_k = temp + 273.15 #K 445 | sigma = numpy.sqrt((kb*temp_k)/m) 446 | return stats.norm(0, sigma).pdf(velocities) 447 | 448 | def MBdist2(velocities, temp = 20, mass = 132.90545): 449 | """ 450 | Method for calculating the relative abundances of given velocity classes 451 | assuming a Maxwell-Boltzmann distribution of velocities for a given 452 | temperature and atomic mass explicitly (without using Scipy). 453 | Defaults to Caesium (the best atom) at room temperature. 454 | 455 | Args: 456 | velocities (1D array): atomic velocities in units of m/s 457 | temp (int, optional): Temperature of the atomic ensemble 458 | in degrees Celsius. Defaults to 20 deg C (room temp). 459 | mass (float, optional): Mass number of the atomic species used 460 | in atomic mass units. Defaults to 132.90545 amu (Caesium). 461 | 462 | Returns: 463 | numpy.ndarray: relative abundances of the specified velocities 464 | according to the Maxwell-Boltzmann distribution. 465 | will be the same shape as velocities. 466 | """ 467 | kb = 1.38e-23 #m**2 kg s**-2 K**-1 468 | m = mass*1.66e-27 #kg 469 | T = temp+273.15 #Kelvin 470 | f = numpy.sqrt(m/(2*numpy.pi*kb*T))*numpy.exp((-m*velocities**2)/(2*kb*T)) 471 | return f 472 | 473 | def time_op(operator, t): 474 | """Creates expresion for the time evolution operator. 475 | 476 | Args: 477 | operator (matrix): Operator describing the time evolution of a system 478 | in matrix form 479 | t (float): Time at which the expression is to be evaluated 480 | 481 | Returns: 482 | numpy.matrix: matrix form of the time evolution operator 483 | exp^{operator*t} 484 | """ 485 | exponent = operator*t 486 | return linalg.expm(exponent) #linalg.expm does matrix exponentiation 487 | 488 | def time_evolve(operator, t, psi_0): 489 | """ 490 | Evaluate the state of a system at a time t, given an operator 491 | describing its time evolution and the state of the system at t=0. 492 | 493 | Args: 494 | operator (matrix): matrix representation of operator describing 495 | time evolution of the system. 496 | t (float): Time at which the state of the system is to be evaluated. 497 | psi_0 (1D array): Vector describing the initial state of the system 498 | (at t=0). 499 | 500 | Returns: 501 | 1D array: the state of the system at time t 502 | """ 503 | return numpy.matmul(time_op(operator, t), psi_0) 504 | 505 | def time_dep_matrix(Omegas, Deltas, Gammas, gammas = []): 506 | """ 507 | Given lists of parameters (all in order of lowers to highest energy level), 508 | construct matrix of coefficients for time evolution of 509 | the density matrix vector. 510 | 511 | Args: 512 | Omegas (list of floats): Rabi frequencies of fields coupling each pair 513 | of states in the ladder, from lowest to highest energy states 514 | (\Omega_{12}, ..., \Omega{n-1,n} for n levels). 515 | Deltas (list of floats): Detuning of fields coupling each pair 516 | of states in the ladder, from lowest to highest energy states 517 | (\Delta_{12}, ..., \Delta{n-1,n} for n levels). 518 | Gammas (list of floats): Linewidth of the atomic states considered. 519 | Assumes no decay from lowest energy atomic state (\Gamma_1 = 0). 520 | Values should be given in order of lowest to highest energy level 521 | (\Gamma_2, \Gamma_3, ... , \Gamma_n for n levels). 522 | n-1 values for n levels. 523 | gammas (list of floats, optional): Linewidths of the fields coupling 524 | each pair of states in the ladder, from lowest to highest energy 525 | (\gamma_{12}, ..., \gamma{n-1,n} for n levels). 526 | Defaults to [] (meaning all \gamma_ij = 0). 527 | Returns: 528 | ndarray: n^2 x n^2 array (for an n-level system) of coefficients M 529 | which satisfies the equation d\rho_{vect}/dt = M\rho_{vect} 530 | where \rho_{vect} is the vector representation of the 531 | density matrix. 532 | """ 533 | # first create decay/dephasing operators 534 | L_atom = L_decay(Gammas) 535 | if len(gammas) != 0: 536 | L_laser = L_dephasing(gammas) 537 | L_tot = L_atom + L_laser 538 | else: 539 | L_tot = L_atom 540 | H = Hamiltonian(Omegas, Deltas) #create the total Hamiltonian 541 | Master = Master_eqn(H, L_tot) 542 | rho_coeffs = OBE_matrix(Master) #create matrix of coefficients 543 | return rho_coeffs 544 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBE (Optical Bloch Equation) Python Tools 2 | 3 | OBE Tools is a collection of functions written in Python that provide solution methods for the optical Bloch equations (OBEs). Alongside the functions available in the `OBE_Tools.py` module are two Jupyer notebooks containing more information on how the functions are set up (`Functions.ipynb`) and examples of how they can be used to model atom-light systems (`Examples.ipynb`). For more information please see the publication _Simple Python tools for modelling few-level atom-light interactions_ [[1]](#References). 4 | 5 | To run the Jupyter notebooks in an interactive browser window, click here: 6 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/LucyDownes/OBE_Python_Tools/HEAD) 7 | 8 | ### Prerequisites 9 | - Numpy 10 | - Scipy (constants, stats, linalg sub-packages) 11 | - Sympy (requires mpmath) 12 | - Matplotlib (for plotting in the `Examples` notebook) 13 | 14 | ## Background 15 | 16 | The optical Bloch equations (OBEs) arise from the Master equation which describes the time dependence of the density matrix through 17 | 18 | $$\frac{\partial \hat{\rho}}{\partial t} = -\frac{i}{\hbar}\left[\hat{H}, \hat{\rho}\right] + \hat{\mathcal{L}}(\hat{\rho})$$ 19 | 20 | where $\hat{H}$ is the Hamiltonian of the atom-light system, $\hat{\rho}$ is the density matrix and $\hat{\mathcal{L}}$ describes the decay and dephasing in the system. 21 | 22 | For systems with fewer than 5 levels, analytic solutions can be found, but these require using approximations and so are only valid within certain parameter ranges. The functions in the `OBE_Tools` module allows the OBEs to be set up and solved numerically for any parameter regime. It uses `Sympy` to derive the OBEs from an interaction Hamiltonian and decay/dephasing operator, then expresses these as matrix equations and uses a singular value decomposition (SVD) to solve them. It also provides functions for the analytic weak-probe solutions. 23 | 24 | ### Notes 25 | 26 | - $\hbar = 1$ 27 | - Parameters are in units of $2\pi\,\rm{MHz}$ (the $2\pi$ is omitted from plots for clarity) 28 | - Examples are purely illustrative and do not represent any particular physical experiment 29 | - The functions are in no way optimised for speed, they are intended to be as transparent as possible to aid insight. 30 | - Variable names have been chosen to align with the notation used in the paper. Parameters (and hence variables) referring to a field coupling two levels $i$ and $j$ with have the subscript $ij$, whereas parameters that are relevant to a single atomic level $n$ will have the single subscript $n$. Variable names with upper/lower-case letters denote upper/lower-case Greek symbols respectively, for example `Gamma` denotes $\Gamma$ while `gamma` denotes $\gamma$. 31 | 32 | The code makes a number of assumptions: 33 | - That the number of atomic levels is equal to 1 plus the number of fields. This is consistent with a ladder system in which each level is coupled to the ones above and below it (except for the top and bottom levels). 34 | - That each level only decays to the level below it, and there is no decay out of the bottom level. 35 | - That laser linewidths do not matter (unless they are explicitly specified). 36 | 37 | ## References 38 | 39 | [1] Lucy A. Downes, *Simple Python tools for modelling few-level atom-light interactions* J. Phys. B: At. Mol. Opt. Phys. **56** 223001 (2023) [https://iopscience.iop.org/article/10.1088/1361-6455/acee3a](https://iopscience.iop.org/article/10.1088/1361-6455/acee3a) 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ## Package requirements for running in Binder 2 | 3 | numpy 4 | scipy 5 | matplotlib 6 | mpmath 7 | sympy 8 | --------------------------------------------------------------------------------