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). 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:
Lastly we compare the results to using the Bloch-Redfield approach:
which does not give the correct evolution in this case.
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$.
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,
Options,
)
from qutip.nonmarkov.heom import (
HEOMSolver,
BosonicBath,
DrudeLorentzBath,
DrudeLorentzPadeBath,
BathExponent,
)
%matplotlib inline
Let's define some helper functions for calculating correlation function expansions, plotting results and timing how long operations take:
def cot(x):
""" Vectorized cotangent of x. """
return 1. / np.tan(x)
@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}")
And let us set up the system Hamiltonian, bath and system measurement operators:
# 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()
# Initial state of the system.
rho0 = basis(2, 0) * basis(2, 0).dag()
# 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)
# 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()
Let us briefly inspect the spectral density.
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);
options = Options(nsteps=15000, store_states=True, rtol=1e-14, atol=1e-14)
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)
RHS construction time: 0.03407168388366699 ODE solver time: 0.6786293983459473
options = Options(nsteps=15000, store_states=True, rtol=1e-14, atol=1e-14)
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)
RHS construction time: 0.030375003814697266 ODE solver time: 0.6803984642028809
# 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);
# 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);
options = Options(nsteps=15000, store_states=True, rtol=1e-14, atol=1e-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)
RHS construction time: 0.033873796463012695 ODE solver time: 0.6489057540893555
# 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);
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 = (
[ans[0] / 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)
# 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))
Correlation (real) fitting time: 0.2524707317352295
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()
# 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]
options = Options(nsteps=1500, store_states=True, rtol=1e-12, atol=1e-12)
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)
RHS construction time: 0.19547080993652344 ODE solver time: 38.62653660774231
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)
options = Options(nsteps=15000, store_states=True, rtol=1e-12, atol=1e-12)
resultBR = brmesolve(
Hsys, rho0, tlist,
a_ops=[[sigmaz(), DL]], sec_cutoff=0, options=options,
)
Finally, let's plot all of our different results to see how they shape up against each other.
# 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))
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,
}
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)
qutip.about()
QuTiP: Quantum Toolbox in Python ================================ Copyright (c) QuTiP team 2011 and later. Current admin team: Alexander Pitchford, Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguère, Boxi Li, Jake Lishman, Simon Cross and Asier Galicia. Board members: Daniel Burgarth, Robert Johansson, Anton F. Kockum, Franco Nori and Will Zeng. Original developers: R. J. Johansson & P. D. Nation. Previous lead developers: Chris Granade & A. Grimsmo. Currently developed through wide collaboration. See https://github.com/qutip for details. QuTiP Version: 4.7.1 Numpy Version: 1.22.4 Scipy Version: 1.8.1 Cython Version: 0.29.33 Matplotlib Version: 3.5.2 Python Version: 3.10.4 Number of CPUs: 2 BLAS Info: Generic OPENMP Installed: False INTEL MKL Ext: False Platform Info: Linux (x86_64) Installation path: /home/runner/work/qutip-tutorials/qutip-tutorials/qutip/qutip ================================================================================ Please cite QuTiP in your publication. ================================================================================ For your convenience a bibtex reference can be easily generated using `qutip.cite()`
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.
assert np.allclose(P11_matsT, P11_pade, rtol=1e-3)
assert np.allclose(P11_matsT, P11_fit, rtol=1e-3)