Skip to content

Commit 629548e

Browse files
authored
Merge pull request #117 from Langhaarzombie/feature/ntbk_nm-mcsolve
QuTiPv5 Paper Notebook: Non-Markovian mcsolve
2 parents 75ddddc + 0c250b8 commit 629548e

File tree

1 file changed

+389
-0
lines changed

1 file changed

+389
-0
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
---
2+
jupyter:
3+
jupytext:
4+
text_representation:
5+
extension: .md
6+
format_name: markdown
7+
format_version: '1.3'
8+
jupytext_version: 1.13.8
9+
kernelspec:
10+
display_name: Python 3 (ipykernel)
11+
language: python
12+
name: python3
13+
---
14+
15+
# QuTiPv5 Paper Example: Monte Carlo Solver for non-Markovian Baths
16+
17+
Authors: Maximilian Meyer-Mölleringhof ([email protected]), Paul Menczel ([email protected]), Neill Lambert ([email protected])
18+
19+
## Introduction
20+
21+
When a quantum system experiences non-Markovian effects, it can no longer be described by the standard Lindblad formalism.
22+
To compute the time evolution of the density matrix, we can however use time-convolutionless (TCL) projection operators that lead to a differential equation in time-local form:
23+
24+
$\dot{\rho} (t) = - \dfrac{i}{\hbar} [H(t), \rho(t)] + \sum_n \gamma_n(t) \mathcal{D}_n(t) [\rho(t)]$
25+
26+
with
27+
28+
$\mathcal{D}_n[\rho(t)] = A_n \rho(t) A^\dagger_n - \dfrac{1}{2} [A^\dagger_n A_n \rho(t) + \rho(t) A_n^\dagger A_n]$.
29+
30+
These equations include the system Hamiltonian $H(t)$ and jump operators $A_n$.
31+
Contrary to a Lindblad equation, the coupling rates $\gamma_n(t)$ may be negative here.
32+
33+
In QuTiP v5, the non-Markovian Monte Carlo solver is introduced.
34+
It enables the mapping of the general master equation given above, to a Lindblad equation on the same Hilbert space.
35+
This is achieved by the introduction of the so called "influence martingale" which acts as trajectory weighting [\[1, 2, 3\]](#References):
36+
37+
$\mu (t) = \exp \left[ \alpha \int_0^t s(\tau) d \tau \right] \Pi_k \dfrac{\gamma_{n_k} (t_k)}{\Gamma_{n_k} (t_k)}$.
38+
39+
Here, the product runs over all jump operators on the trajectory with jump channels $n_k$ and jump times $t_k < t$.
40+
To finally arrive at the Lindblad form, the shift function
41+
42+
$s(t) = 2 \left| \min \{ 0, \gamma_1(t), \gamma_2(t), ... \} \right|$
43+
44+
is applied, such that the shifted rates $\Gamma_n (t) = \gamma_n(t) + s(t)$ are non-negative.
45+
We obtain the completely positive Lindblad equation
46+
47+
$\dot{\rho}'(t) = - \dfrac{i}{\hbar} [ H(t), \rho'(t) ] + \sum_n \Gamma(t) \mathcal{D}_n[\rho'(t)]$,
48+
49+
and $\rho'(t) = \mathbb{E} \{\ket{\psi(t)} \bra{\psi(t)}\}$ using the regular MCWF method.
50+
Here, $\ket{\psi (t)}$ are the generated trajectories and $\mathbb{E}$ is the ensemble average over the trajectories.
51+
This can furthermore be used to finally reconstruct the original states via $\rho(t) = \mathbb{E}\{\mu(t) \ket{\psi(t)} \bra{\psi(t)}\}$.
52+
53+
Note that, for this technique, the jump operators are required to fulfill the completeness relation $\sum_n A_n^\dagger A_n = \alpha \mathbb{1}$ for $\alpha > 0$.
54+
This condition is automatically taken care of by QuTiP's `nm_mcsolve()` function.
55+
56+
```python
57+
import matplotlib.pyplot as plt
58+
import numpy as np
59+
from qutip import (ExponentialBosonicEnvironment, about, basis, brmesolve,
60+
expect, heom, ket2dm, lindblad_dissipator, liouvillian,
61+
mesolve, nm_mcsolve, qeye, sigmam, sigmap, sigmax, sigmay,
62+
sigmaz)
63+
from scipy.interpolate import CubicSpline
64+
from scipy.optimize import root_scalar
65+
```
66+
67+
## Damped Jaynes-Cumming Model
68+
69+
To illustrate the application of `nm_mcsolve()`, we want to look at the damped Jaynes-Cumming model.
70+
It describes a two-level atom coupled to a damped cavity mode.
71+
Such a model can be accurately model as a two-level system coupled to an environment with the power spectrum [\[4\]](#References):
72+
73+
$S(\omega) = \dfrac{\lambda \Gamma^2}{(\omega_0 - \Delta - \omega)^2 + \Gamma^2}$,
74+
75+
where $\lambda$ is the atom-cavity coupling strength, $\omega_0$ is the transition frequency of the atom, $\Delta$ the detuning of the cavity and $\Gamma$ the spectral width.
76+
At zero temperature and after performing the rotating wave approximation, the dynamics of the two-level atom can be described by the master equation
77+
78+
$\dot{\rho}(t) = \dfrac{A(t)}{2i\hbar} [ \sigma_+ \sigma_-, \rho(t) ] + \gamma (t) \mathcal{D}_- [\rho (t)]$,
79+
80+
with the state $\rho(t)$ in the interaction picture, the the ladder operators $\sigma_\pm$, the dissipator $\mathcal{D}_-$ for the Lindblad operator $\sigma_-$, and lastly $\gamma (t)$ and $A(t)$ being the real and imaginary parts of
81+
82+
$\gamma(t) + i A(t) = \dfrac{2 \lambda \Gamma \sinh (\delta t / 2)}{\delta \cosh (\delta t / 2) + (\Gamma - i \Delta) \sinh (\delta t/2)}$,
83+
84+
with $\delta = [(\Gamma - i \Delta)^2 - 2 \lambda \Gamma]^{1/2}$.
85+
86+
```python
87+
H = sigmap() * sigmam() / 2
88+
initial_state = (basis(2, 0) + basis(2, 1)).unit()
89+
tlist = np.linspace(0, 5, 500)
90+
```
91+
92+
```python
93+
# Constants
94+
gamma0 = 1
95+
lamb = 0.3 * gamma0
96+
Delta = 8 * lamb
97+
98+
# Derived Quantities
99+
delta = np.sqrt((lamb - 1j * Delta) ** 2 - 2 * gamma0 * lamb)
100+
deltaR = np.real(delta)
101+
deltaI = np.imag(delta)
102+
deltaSq = deltaR**2 + deltaI**2
103+
```
104+
105+
```python
106+
# calculate gamma and A
107+
def prefac(t):
108+
return (
109+
2
110+
* gamma0
111+
* lamb
112+
/ (
113+
(lamb**2 + Delta**2 - deltaSq) * np.cos(deltaI * t)
114+
- (lamb**2 + Delta**2 + deltaSq) * np.cosh(deltaR * t)
115+
- 2 * (Delta * deltaR + lamb * deltaI) * np.sin(deltaI * t)
116+
+ 2 * (Delta * deltaI - lamb * deltaR) * np.sinh(deltaR * t)
117+
)
118+
)
119+
120+
121+
def cgamma(t):
122+
return prefac(t) * (
123+
lamb * np.cos(deltaI * t)
124+
- lamb * np.cosh(deltaR * t)
125+
- deltaI * np.sin(deltaI * t)
126+
- deltaR * np.sinh(deltaR * t)
127+
)
128+
129+
130+
def cA(t):
131+
return prefac(t) * (
132+
Delta * np.cos(deltaI * t)
133+
- Delta * np.cosh(deltaR * t)
134+
- deltaR * np.sin(deltaI * t)
135+
+ deltaI * np.sinh(deltaR * t)
136+
)
137+
```
138+
139+
```python
140+
_gamma = np.zeros_like(tlist)
141+
_A = np.zeros_like(tlist)
142+
for i in range(len(tlist)):
143+
_gamma[i] = cgamma(tlist[i])
144+
_A[i] = cA(tlist[i])
145+
146+
gamma = CubicSpline(tlist, np.complex128(_gamma))
147+
A = CubicSpline(tlist, np.complex128(_A))
148+
```
149+
150+
```python
151+
unitary_gen = liouvillian(H)
152+
dissipator = lindblad_dissipator(sigmam())
153+
```
154+
155+
```python
156+
mc_sol = nm_mcsolve(
157+
[[H, A]],
158+
initial_state,
159+
tlist,
160+
ops_and_rates=[(sigmam(), gamma)],
161+
ntraj=1_000,
162+
options={"map": "parallel"},
163+
seeds=0,
164+
)
165+
```
166+
167+
## Comparison to other methods
168+
169+
We want to compare this computation with the standard `mesolve()`, the HEOM and the Bloch-Refield solver.
170+
For the `mesolve()` we can directly use the operators we have created for `nm_mcsolve()`:
171+
172+
```python
173+
me_sol = mesolve([[unitary_gen, A], [dissipator, gamma]], initial_state, tlist)
174+
```
175+
176+
For the other methods we directly apply a spin-boson model and the free reservoir auto-correlation function
177+
178+
$C(t) = \dfrac{\lambda \Gamma}{2} e^{-i (\omega - \Delta) t - \lambda |t|}$,
179+
180+
which corresponds to the power spectrum defined above.
181+
We use the Hamiltonian $H = \omega_0 \sigma_+ \sigma_-$ in the Schrödinger picture and the coupling operator $Q = \sigma_+ + \sigma_-$.
182+
Here, we chose $\omega_0 \gg \Delta$ to ensure validity of the rotating wave approximation.
183+
184+
```python
185+
omega_c = 100
186+
omega_0 = omega_c + Delta
187+
188+
H = omega_0 * sigmap() * sigmam()
189+
Q = sigmap() + sigmam()
190+
191+
192+
def power_spectrum(w):
193+
return gamma0 * lamb**2 / ((omega_c - w) ** 2 + lamb**2)
194+
```
195+
196+
Firstly, the HEOM solver can directly be applied after separating the auto-correlation function into its real and imaginary parts:
197+
198+
```python
199+
ck_real = [gamma0 * lamb / 4] * 2
200+
vk_real = [lamb - 1j * omega_c, lamb + 1j * omega_c]
201+
ck_imag = np.array([1j, -1j]) * gamma0 * lamb / 4
202+
vk_imag = vk_real
203+
```
204+
205+
```python
206+
heom_env = ExponentialBosonicEnvironment(ck_real, vk_real, ck_imag, vk_imag)
207+
heom_sol = heom.heomsolve(H, (heom_env, Q), 10, ket2dm(initial_state), tlist)
208+
```
209+
210+
Secondly, for the Bloch-Redfield solver we can directly use the power spectrum as input:
211+
212+
```python
213+
br_sol = brmesolve(H, initial_state, tlist, a_ops=[(sigmax(), power_spectrum)])
214+
```
215+
216+
Finally, in order to compare these results with the `nm_mesolve()` method, we transform them to the interaction picture:
217+
218+
```python
219+
Us = [(-1j * H * t).expm() for t in tlist]
220+
heom_states = [U * s * U.dag() for (U, s) in zip(Us, heom_sol.states)]
221+
br_states = [U * s * U.dag() for (U, s) in zip(Us, br_sol.states)]
222+
```
223+
224+
### Plotting the Time Evolution
225+
226+
We choose to plot three comparisons to highlight the different solvers' computational results.
227+
In all plots, the gray areas show when $\gamma(t)$ is negative.
228+
229+
```python
230+
root1 = root_scalar(lambda t: cgamma(t), method="bisect", bracket=(1, 2)).root
231+
root2 = root_scalar(lambda t: cgamma(t), method="bisect", bracket=(2, 3)).root
232+
root3 = root_scalar(lambda t: cgamma(t), method="bisect", bracket=(3, 4)).root
233+
root4 = root_scalar(lambda t: cgamma(t), method="bisect", bracket=(4, 5)).root
234+
```
235+
236+
```python
237+
projector = (sigmaz() + qeye(2)) / 2
238+
239+
rho11_me = expect(projector, me_sol.states)
240+
rho11_mc = expect(projector, mc_sol.states)
241+
rho11_br = expect(projector, br_sol.states)
242+
rho11_heom = expect(projector, heom_states)
243+
244+
plt.plot(tlist, rho11_me, "-", color="orange", label="mesolve")
245+
plt.plot(
246+
tlist[::10],
247+
rho11_mc[::10],
248+
"x",
249+
color="blue",
250+
label="nm_mcsolve",
251+
)
252+
plt.plot(tlist, rho11_br, "-.", color="gray", label="brmesolve")
253+
plt.plot(tlist, rho11_heom, "--", color="green", label="heomsolve")
254+
255+
plt.xlabel(r"$t\, /\, \lambda^{-1}$")
256+
plt.xlim((-0.2, 5.2))
257+
plt.xticks([0, 2.5, 5], labels=["0", "2.5", "5"])
258+
plt.title(r"$\rho_{11}$")
259+
plt.ylim((0.4376, 0.5024))
260+
plt.yticks([0.44, 0.46, 0.48, 0.5], labels=["0.44", "0.46", "0.48", "0.50"])
261+
262+
plt.axvspan(root1, root2, color="gray", alpha=0.08, zorder=0)
263+
plt.axvspan(root3, root4, color="gray", alpha=0.08, zorder=0)
264+
265+
plt.legend()
266+
plt.show()
267+
```
268+
269+
```python
270+
me_x = expect(sigmax(), me_sol.states)
271+
mc_x = expect(sigmax(), mc_sol.states)
272+
heom_x = expect(sigmax(), heom_states)
273+
br_x = expect(sigmax(), br_states)
274+
275+
me_y = expect(sigmay(), me_sol.states)
276+
mc_y = expect(sigmay(), mc_sol.states)
277+
heom_y = expect(sigmay(), heom_states)
278+
br_y = expect(sigmay(), br_states)
279+
```
280+
281+
```python
282+
# We smooth the HEOM result because it oscillates quickly and gets hard to see
283+
rho01_heom = heom_x * heom_x + heom_y * heom_y
284+
rho01_heom = np.convolve(rho01_heom, np.array([1 / 11] * 11), mode="valid")
285+
heom_tlist = tlist[5:-5]
286+
```
287+
288+
```python
289+
rho01_me = me_x * me_x + me_y * me_y
290+
rho01_mc = mc_x * mc_x + mc_y * mc_y
291+
rho01_br = br_x * br_x + br_y * br_y
292+
293+
plt.plot(tlist, rho01_me, "-", color="orange", label=r"mesolve")
294+
plt.plot(tlist[::10], rho01_mc[::10], "x", color="blue", label=r"nm_mcsolve")
295+
plt.plot(heom_tlist, rho01_heom, "--", color="green", label=r"heomsolve")
296+
plt.plot(tlist, rho01_br, "-.", color="gray", label=r"brmesolve")
297+
298+
plt.xlabel(r"$t\, /\, \lambda^{-1}$")
299+
plt.xlim((-0.2, 5.2))
300+
plt.xticks([0, 2.5, 5], labels=["0", "2.5", "5"])
301+
plt.title(r"$| \rho_{01} |^2$")
302+
plt.ylim((0.8752, 1.0048))
303+
plt.yticks(
304+
[0.88, 0.9, 0.92, 0.94, 0.96, 0.98, 1],
305+
labels=["0.88", "0.90", "0.92", "0.94", "0.96", "0.98", "1"],
306+
)
307+
308+
plt.axvspan(root1, root2, color="gray", alpha=0.08, zorder=0)
309+
plt.axvspan(root3, root4, color="gray", alpha=0.08, zorder=0)
310+
311+
plt.legend()
312+
plt.show()
313+
```
314+
315+
```python
316+
mart_dev = mc_sol.trace - 1
317+
```
318+
319+
```python
320+
plt.plot(tlist, np.zeros_like(tlist), "-", color="orange", label=r"Zero")
321+
plt.plot(
322+
tlist[::10],
323+
1000 * mart_dev[::10],
324+
"x",
325+
color="blue",
326+
label=r"nm_mcsolve",
327+
)
328+
329+
plt.xlabel(r"$t\, /\, \lambda^{-1}$")
330+
plt.xlim((-0.2, 5.2))
331+
plt.xticks([0, 2.5, 5], labels=["0", "2.5", "5"])
332+
plt.title(r"$(\mu - 1)\, /\, 10^{-3}$")
333+
plt.ylim((-5.8, 15.8))
334+
plt.yticks([-5, 0, 5, 10, 15])
335+
336+
plt.axvspan(root1, root2, color="gray", alpha=0.08, zorder=0)
337+
plt.axvspan(root3, root4, color="gray", alpha=0.08, zorder=0)
338+
339+
plt.legend()
340+
plt.show()
341+
```
342+
343+
In these plots we notice two things.
344+
First, the result from the Bloch-Redfield equation deviates greatly from the others, showing us that non-Markovian effects are strongly influencing the dynamics.
345+
Second, in the grey areas - when $\gamma(t)$ is negative - the atom state restores coherence.
346+
In the last plot especially we see that during these times, the average influence martingale fluctuates, but is constant otherwise.
347+
Its deviation from unity tells us how well the simulation has converged.
348+
349+
350+
351+
## References
352+
353+
\[1\] [Donvil and Muratore-Ginanneschi. Nat Commun (2022).](https://www.nature.com/articles/s41467-022-31533-8)
354+
355+
\[2\] [Donvil and Muratore-Ginanneschi. New J. Phys. (2023).](https://dx.doi.org/10.1088/1367-2630/acd4dc)
356+
357+
\[3\] [Donvil and Muratore-Ginanneschi. *Open Systems & Information Dynamics*.](https://www.worldscientific.com/worldscinet/osid)
358+
359+
\[4\] [Breuer and Petruccione *The Theory of Open Quantum Systems*.](https://doi.org/10.1093/acprof:oso/9780199213900.001.0001)
360+
361+
\[5\] [QuTiP 5: The Quantum Toolbox in Python](https://arxiv.org/abs/2412.04705)
362+
363+
364+
365+
## About
366+
367+
```python
368+
about()
369+
```
370+
371+
## Testing
372+
373+
```python
374+
assert np.allclose(
375+
rho11_me, rho11_heom, atol=1e-3
376+
), "rho11 of mesolve and heomsolve do not agree."
377+
assert np.allclose(
378+
rho11_me, rho11_mc, atol=1e-2
379+
), "rho11 of nm_mcsolve deviates from mesolve too much."
380+
assert np.allclose(
381+
rho01_me[5:-5], rho01_heom, atol=1e-3
382+
), "|rho01|^2 of mesolve and heomsolve do not agree."
383+
assert np.allclose(
384+
rho01_me, rho01_mc, atol=1e-1
385+
), "|rho01|^2 of nm_mcsolve deviates from mesolve too much."
386+
assert (
387+
np.max(mart_dev) < 1e-1
388+
), "MC Simulation has not converged well enough. Average infl. mart. > 1e-1"
389+
```

0 commit comments

Comments
 (0)