#!/usr/bin/env python # coding: utf-8 # # HEOM 1b: Spin-Bath model (very strong coupling) # ## Introduction # # The HEOM method solves the dynamics and steady state of a system and its environment, the latter of which is encoded in a set of auxiliary density matrices. # # In this example we show the evolution of a single two-level system in contact with a single Bosonic environment. The properties of the system are encoded in Hamiltonian, and a coupling operator which describes how it is coupled to the environment. # # The Bosonic environment is implicitly assumed to obey a particular Hamiltonian, the parameters of which are encoded in the spectral density, and subsequently the free-bath correlation functions. # # In the example below we show how to model the overdamped Drude-Lorentz Spectral Density, commonly used with the HEOM. We show how to do this using the Matsubara, Pade and fitting decompositions, and compare their convergence. # # This notebook shows a similar example to notebook 1a, but with much stronger coupling as discussed in [Shi *et al.*, J. Chem. Phys **130**, 084105 (2009)](https://doi.org/10.1063/1.3077918). Please refer to notebook HEOM 1a for a more detailed explanation. # # As in notebook 1a, we present a variety of simulations using different techniques to showcase the effect of different approximations of the correlation function on the results: # # - Simulation 1: Matsubara decomposition, not using Ishizaki-Tanimura terminator # - Simulation 2: Matsubara decomposition (including terminator) # - Simulation 3: Pade decomposition # - Simulation 4: Fitting approach # # Lastly we compare the results to using the Bloch-Redfield approach: # # - Simulation 5: Bloch-Redfield # # which does not give the correct evolution in this case. # # # ### Drude-Lorentz (overdamped) spectral density # # The Drude-Lorentz spectral density is: # # $$J_D(\omega)= \frac{2\omega\lambda\gamma}{{\gamma}^2 + \omega^2}$$ # # where $\lambda$ scales the coupling strength, and $\gamma$ is the cut-off frequency. We use the convention, # \begin{equation*} # C(t) = \int_0^{\infty} d\omega \frac{J_D(\omega)}{\pi}[\coth(\beta\omega) \cos(\omega \tau) - i \sin(\omega \tau)] # \end{equation*} # # With the HEOM we must use an exponential decomposition: # # \begin{equation*} # C(t)=\sum_{k=0}^{k=\infty} c_k e^{-\nu_k t} # \end{equation*} # # As an example, the Matsubara decomposition of the Drude-Lorentz spectral density is given by: # # \begin{equation*} # \nu_k = \begin{cases} # \gamma & k = 0\\ # {2 \pi k} / {\beta } & k \geq 1\\ # \end{cases} # \end{equation*} # # \begin{equation*} # c_k = \begin{cases} # \lambda \gamma (\cot(\beta \gamma / 2) - i) & k = 0\\ # 4 \lambda \gamma \nu_k / \{(nu_k^2 - \gamma^2)\beta \} & k \geq 1\\ # \end{cases} # \end{equation*} # # Note that in the above, and the following, we set $\hbar = k_\mathrm{B} = 1$. # ## Setup # In[1]: import contextlib import time import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt import qutip from qutip import ( basis, brmesolve, expect, liouvillian, sigmax, sigmaz, ) from qutip.solver.heom import ( HEOMSolver, BosonicBath, DrudeLorentzBath, DrudeLorentzPadeBath, BathExponent, ) get_ipython().run_line_magic('matplotlib', 'inline') # ## Helper functions # # Let's define some helper functions for calculating correlation function expansions, plotting results and timing how long operations take: # In[2]: def cot(x): """ Vectorized cotangent of x. """ return 1. / np.tan(x) # In[3]: @contextlib.contextmanager def timer(label): """ Simple utility for timing functions: with timer("name"): ... code to time ... """ start = time.time() yield end = time.time() print(f"{label}: {end - start}") # In[4]: # Solver options: options = { "nsteps": 15000, "store_states": True, "rtol": 1e-14, "atol": 1e-14, "method": "vern9", "progress_bar": "enhanced", } # ## System and bath definition # # And let us set up the system Hamiltonian, bath and system measurement operators: # In[5]: # Defining the system Hamiltonian eps = .0 # Energy of the 2-level system. Del = .2 # Tunnelling term Hsys = 0.5 * eps * sigmaz() + 0.5 * Del * sigmax() # In[6]: # Initial state of the system. rho0 = basis(2, 0) * basis(2, 0).dag() # In[7]: # System-bath coupling (Drude-Lorentz spectral density) Q = sigmaz() # coupling operator # Bath properties (see Shi et al., J. Chem. Phys. 130, 084105 (2009)): gamma = 1. # cut off frequency lam = 2.5 # coupling strength T = 1. # in units where Boltzmann factor is 1 beta = 1. / T # HEOM parameters: # number of exponents to retain in the Matsubara expansion of the # bath correlation function: Nk = 1 # Number of levels of the hierarchy to retain: NC = 13 # Times to solve for: tlist = np.linspace(0, np.pi / Del, 600) # In[8]: # Define some operators with which we will measure the system # 1,1 element of density matrix - corresonding to groundstate P11p = basis(2, 0) * basis(2, 0).dag() P22p = basis(2, 1) * basis(2, 1).dag() # 1,2 element of density matrix - corresonding to coherence P12p = basis(2, 0) * basis(2, 1).dag() # ### Plot the spectral density # # Let us briefly inspect the spectral density. # In[9]: w = np.linspace(0, 5, 1000) J = w * 2 * lam * gamma / ((gamma**2 + w**2)) # Plot the results fig, axes = plt.subplots(1, 1, sharex=True, figsize=(8, 8)) axes.plot(w, J, 'r', linewidth=2) axes.set_xlabel(r'$\omega$', fontsize=28) axes.set_ylabel(r'J', fontsize=28); # ## Simulation 1: Matsubara decomposition, not using Ishizaki-Tanimura terminator # In[10]: with timer("RHS construction time"): bath = DrudeLorentzBath(Q, lam=lam, gamma=gamma, T=T, Nk=Nk) HEOMMats = HEOMSolver(Hsys, bath, NC, options=options) with timer("ODE solver time"): resultMats = HEOMMats.run(rho0, tlist) # ## Simulation 2: Matsubara decomposition (including terminator) # In[11]: with timer("RHS construction time"): bath = DrudeLorentzBath(Q, lam=lam, gamma=gamma, T=T, Nk=Nk) _, terminator = bath.terminator() Ltot = liouvillian(Hsys) + terminator HEOMMatsT = HEOMSolver(Ltot, bath, NC, options=options) with timer("ODE solver time"): resultMatsT = HEOMMatsT.run(rho0, tlist) # In[12]: # Plot the results fig, axes = plt.subplots(1, 1, sharex=True, figsize=(8, 8)) P11_mats = np.real(expect(resultMats.states, P11p)) axes.plot( tlist, np.real(P11_mats), 'b', linewidth=2, label="P11 (Matsubara)", ) P11_matsT = np.real(expect(resultMatsT.states, P11p)) axes.plot( tlist, np.real(P11_matsT), 'b--', linewidth=2, label="P11 (Matsubara + Terminator)", ) axes.set_xlabel(r't', fontsize=28) axes.legend(loc=0, fontsize=12); # ## Simulation 3: Pade decomposition # In[13]: # First, compare Matsubara and Pade decompositions matsBath = DrudeLorentzBath(Q, lam=lam, gamma=gamma, T=T, Nk=Nk) padeBath = DrudeLorentzPadeBath(Q, lam=lam, gamma=gamma, T=T, Nk=Nk) # We will compare against a summation of {lmaxmats} Matsubara terms lmaxmats = 15000 exactBath = DrudeLorentzBath( Q, lam=lam, gamma=gamma, T=T, Nk=lmaxmats, combine=False, ) def CR(bath, t): """ C_R, the real part of the correlation function. """ result = 0 for exp in bath.exponents: if ( exp.type == BathExponent.types['R'] or exp.type == BathExponent.types['RI'] ): result += exp.ck * np.exp(-exp.vk * t) return result def CI(bath, t): """ C_I, the imaginary part of the correlation function. """ result = 0 for exp in bath.exponents: if exp.type == BathExponent.types['I']: result += exp.ck * np.exp(exp.vk * t) if exp.type == BathExponent.types['RI']: result += exp.ck2 * np.exp(exp.vk * t) return result fig, (ax1, ax2) = plt.subplots(ncols=2, sharey=True, figsize=(16, 8)) ax1.plot( tlist, CR(exactBath, tlist), "r", linewidth=2, label=f"Mats (Nk={lmaxmats})", ) ax1.plot( tlist, CR(matsBath, tlist), "g--", linewidth=2, label=f"Mats (Nk={Nk})", ) ax1.plot( tlist, CR(padeBath, tlist), "b--", linewidth=2, label=f"Pade (Nk={Nk})", ) ax1.set_xlabel(r't', fontsize=28) ax1.set_ylabel(r"$C_R(t)$", fontsize=28) ax1.legend(loc=0, fontsize=12) tlist2 = tlist[0:50] ax2.plot( tlist2, np.abs(CR(matsBath, tlist2) - CR(exactBath, tlist2)), "g", linewidth=2, label="Mats Error", ) ax2.plot( tlist2, np.abs(CR(padeBath, tlist2) - CR(exactBath, tlist2)), "b--", linewidth=2, label="Pade Error", ) ax2.set_xlabel(r't', fontsize=28) ax2.legend(loc=0, fontsize=12); # In[14]: with timer("RHS construction time"): bath = DrudeLorentzPadeBath(Q, lam=lam, gamma=gamma, T=T, Nk=Nk) HEOMPade = HEOMSolver(Hsys, bath, NC, options=options) with timer("ODE solver time"): resultPade = HEOMPade.run(rho0, tlist) # In[15]: # Plot the results fig, axes = plt.subplots(figsize=(8, 8)) axes.plot( tlist, np.real(P11_mats), 'b', linewidth=2, label="P11 (Matsubara)", ) axes.plot( tlist, np.real(P11_matsT), 'b--', linewidth=2, label="P11 (Matsubara + Terminator)", ) P11_pade = np.real(expect(resultPade.states, P11p)) axes.plot( tlist, np.real(P11_pade), 'r', linewidth=2, label="P11 (Pade)", ) axes.set_xlabel(r't', fontsize=28) axes.legend(loc=0, fontsize=12); # ## Simulation 4: Fitting approach # In[16]: def wrapper_fit_func(x, N, args): """ Fit function wrapper that unpacks its arguments. """ x = np.array(x) a = np.array(args[:N]) b = np.array(args[N:2 * N]) return fit_func(x, a, b) def fit_func(x, a, b): """ Fit function. Calculates the value of the correlation function at each x, given the fit parameters in a and b. """ return np.sum( a[:, None] * np.exp(np.multiply.outer(b, x)), axis=0, ) def fitter(ans, tlist, k): """ Compute fit with k exponents. """ upper_a = abs(max(ans, key=abs)) * 10 # sets initial guesses: guess = ( [upper_a / k] * k + # guesses for a [0] * k # guesses for b ) # sets lower bounds: b_lower = ( [-upper_a] * k + # lower bounds for a [-np.inf] * k # lower bounds for b ) # sets higher bounds: b_higher = ( [upper_a] * k + # upper bounds for a [0] * k # upper bounds for b ) param_bounds = (b_lower, b_higher) p1, p2 = curve_fit( lambda x, *params_0: wrapper_fit_func(x, k, params_0), tlist, ans, p0=guess, sigma=[0.01 for t in tlist], bounds=param_bounds, maxfev=1e8, ) a, b = p1[:k], p1[k:] return (a, b) # In[17]: # Fitting the real part of the correlation function: # Correlation function values to fit: tlist_fit = np.linspace(0, 6, 10000) corrRana = CR(exactBath, tlist_fit) # Perform the fit: kR = 3 # number of exponents to use for real part poptR = [] with timer("Correlation (real) fitting time"): for i in range(kR): poptR.append(fitter(corrRana, tlist_fit, i + 1)) # In[18]: plt.plot(tlist_fit, corrRana, label="Analytic") for i in range(kR): y = fit_func(tlist_fit, *poptR[i]) plt.plot(tlist_fit, y, label=f"Fit with {i} terms") plt.title("Fit to correlations (real part)") plt.legend() plt.show() # In[19]: # Set the exponential coefficients from the fit parameters ckAR1 = poptR[-1][0] ckAR = [x + 0j for x in ckAR1] vkAR1 = poptR[-1][1] vkAR = [-x + 0j for x in vkAR1] # Imaginary part: use analytical value ckAI = [lam * gamma * (-1.0) + 0j] vkAI = [gamma + 0j] # In[20]: with timer("RHS construction time"): bath = BosonicBath(Q, ckAR, vkAR, ckAI, vkAI) # We reduce NC slightly here for speed of execution because we retain # 3 exponents in ckAR instead of 1. Please restore full NC for # convergence though: HEOMFit = HEOMSolver(Hsys, bath, int(NC * 0.7), options=options) with timer("ODE solver time"): resultFit = HEOMFit.run(rho0, tlist) # ## Simulation 5: Bloch-Redfield # In[21]: DL = ( "2 * pi * 2.0 * {lam} / (pi * {gamma} * {beta}) if (w==0) " "else 2 * pi * (2.0 * {lam} * {gamma} * w / (pi * (w**2 + {gamma}**2))) " "* ((1 / (exp(w * {beta}) - 1)) + 1)" ).format(gamma=gamma, beta=beta, lam=lam) with timer("ODE solver time"): resultBR = brmesolve( Hsys, rho0, tlist, a_ops=[[sigmaz(), DL]], sec_cutoff=0, options=options, ) # ## Let's plot all our results # # Finally, let's plot all of our different results to see how they shape up against each other. # In[22]: # Calculate expectation values in the bases: P11_mats = np.real(expect(resultMats.states, P11p)) P11_matsT = np.real(expect(resultMatsT.states, P11p)) P11_pade = np.real(expect(resultPade.states, P11p)) P11_fit = np.real(expect(resultFit.states, P11p)) P11_br = np.real(expect(resultBR.states, P11p)) # In[23]: rcParams = { "axes.titlesize": 25, "axes.labelsize": 30, "xtick.labelsize": 28, "ytick.labelsize": 28, "legend.fontsize": 28, "axes.grid": False, "savefig.bbox": "tight", "lines.markersize": 5, "font.family": "STIXgeneral", "mathtext.fontset": "stix", "font.serif": "STIX", "text.usetex": False, } # In[24]: fig, axes = plt.subplots(1, 1, sharex=True, figsize=(12, 7)) with plt.rc_context(rcParams): # Plot the results plt.yticks([0.99, 1.0], [0.99, 1]) axes.plot( tlist, np.real(P11_mats), 'b', linewidth=2, label=f"Matsubara $N_k={Nk}$", ) axes.plot( tlist, np.real(P11_matsT), 'g--', linewidth=3, label=f"Matsubara $N_k={Nk}$ & terminator", ) axes.plot( tlist, np.real(P11_pade), 'y-.', linewidth=2, label=f"Padé $N_k={Nk}$", ) axes.plot( tlist, np.real(P11_fit), 'r', dashes=[3, 2], linewidth=2, label=r"Fit $N_f = 3$, $N_k=15 \times 10^3$", ) axes.plot( tlist, np.real(P11_br), 'b-.', linewidth=1, label="Bloch Redfield", ) axes.locator_params(axis='y', nbins=6) axes.locator_params(axis='x', nbins=6) axes.set_ylabel(r'$\rho_{11}$', fontsize=30) axes.set_xlabel(r'$t\;\gamma$', fontsize=30) axes.set_xlim(tlist[0], tlist[-1]) axes.set_ylim(0.98405, 1.0005) axes.legend(loc=0) # ## About # In[25]: qutip.about() # ## Testing # # This section can include some tests to verify that the expected outputs are generated within the notebook. We put this section at the end of the notebook, so it's not interfering with the user experience. Please, define the tests using assert, so that the cell execution fails if a wrong output is generated. # In[26]: assert np.allclose(P11_matsT, P11_pade, rtol=1e-3) assert np.allclose(P11_matsT, P11_fit, rtol=1e-3)