|
| 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