Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QuTiPv5 Paper Notebook: Floquet formalism and speed comparison #114

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
332 changes: 332 additions & 0 deletions tutorials-v5/time-evolution/024_v5_paper-floquet-speed-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
---
jupyter:
jupytext:
text_representation:
extension: .md
format_name: markdown
format_version: '1.3'
jupytext_version: 1.13.8
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# QuTiPv5 Paper Example: Floquet Speed Test

Authors: Maximilian Meyer-Mölleringhof ([email protected]), Neill Lambert ([email protected])

## Introdcution

In this example we will discuss Hamiltonians with period time-dependence and how we can solve the time evolution of the system in QuTiP.
A natural approach to this is using the Floquet theorem.
Similar to the Block theorem (for spatial periodicity), this method helps to drastically simplify the problem.
Let $H$ be a Hamiltonian periodic in time with period $T$, such that

$H(t) = H(t + T)$

The Floquet theorem states that there exist solutions to the Schrödinger equation that take the form

$\ket{\psi_\alpha (t)} = \exp(-i \epsilon_\alpha t / \hbar) \ket{\Psi_\alpha (t)}$,

where $\epsilon_\alpha$ are the quasi-energies and $\ket{\Psi_\alpha (t)} = \ket{\Psi_\alpha (t + T)}$ the Floquet modes.
We can then write any solution of the time-dependent Schrödinger equation as a linear combination of them

$\ket{\psi(t)} = \sum_\alpha c_\alpha \ket{\Psi_\alpha}$,

where the constants $c_\alpha$ are determined by the initial conditions.

Inserting the Floquet states above into the Schrödinger equation, we can define the Floquet Hamiltonian,

$H_F(t) = H(t) - i \hbar \dfrac{\partial}{\partial t}$,

which renders the problem time-independent,

$H_F(t) \ket{\Psi_\alpha(t)} = \epsilon_\alpha \ket{\Psi_\alpha(t)}$.

Solving this equation then allows us to find $\ket{\psi (t)}$ at any large $t$.

Our goal in this notebook is to show, how this prcedure is done in QuTiP and how it compares to the standard `sesolve` method - specifically in terms of computation time.

```python
import time

import matplotlib.pyplot as plt
import numpy as np
from qutip import (CoreOptions, FloquetBasis, about, basis, expect, qeye,
qzero_like, sesolve, sigmax, sigmay, sigmaz, tensor)

%matplotlib inline
```

## Two-level system with periodic drive

We consider a two-level system that is exposed to a periodic drive:

$H(t) = - \dfrac{\epsilon}{2} \sigma_z - \dfrac{\Delta}{2} \sigma_x + \dfrac{A}{2} \sin(\omega_d t) \sigma_x$,

where $\epsilon$ is the energy splitting, $\Delta$ the coupling strength and $A$ the drive amplitude.

```python
delta = 0.2 * 2 * np.pi
epsilon = 1.0 * 2 * np.pi
A = 2.5 * 2 * np.pi
omega = 1.0 * 2 * np.pi
T = 2 * np.pi / omega

H0 = -1 / 2 * (epsilon * sigmaz() + delta * sigmax())
H1 = A / 2 * sigmax()
args = {"w": omega}
H = [H0, [H1, lambda t, w: np.sin(w * t)]]
```

```python
psi0 = basis(2, 0) # initial condition

# Numerical parameters
dt = 0.01
N_T = 10 # Number of periods
tlist = np.arange(0.0, N_T * T, dt)
```

```python
computational_time_floquet = np.zeros_like(tlist)
computational_time_sesolve = np.zeros_like(tlist)

expect_floquet = np.zeros_like(tlist)
expect_sesolve = np.zeros_like(tlist)
```

```python
# Running the simulation
for n, t in enumerate(tlist):
# Floquet basis
# --------------------------------
tic_f = time.perf_counter()
floquetbasis = FloquetBasis(H, T, args)
# Decomposing inital state into Floquet modes
f_coeff = floquetbasis.to_floquet_basis(psi0)
# Obtain evolved state in the original basis
psi_t = floquetbasis.from_floquet_basis(f_coeff, t)
p_ex = expect(sigmaz(), psi_t)
toc_f = time.perf_counter()

# Saving data
computational_time_floquet[n] = toc_f - tic_f
expect_floquet[n] = p_ex

# sesolve
# --------------------------------
tic_f = time.perf_counter()
psi_se = sesolve(H, psi0, tlist[: n + 1], e_ops=[sigmaz()], args=args)
p_ex_ref = psi_se.expect[0]
toc_f = time.perf_counter()

# Saving data
computational_time_sesolve[n] = toc_f - tic_f
expect_sesolve[n] = p_ex_ref[-1]
```

```python
fig, axs = plt.subplots(1, 2, figsize=(13.6, 4.5))
axs[0].plot(tlist / T, computational_time_floquet, "-")
axs[0].plot(tlist / T, computational_time_sesolve, "--")

axs[0].set_yscale("log")
axs[0].set_yticks(np.logspace(-3, -1, 3))
axs[1].plot(tlist / T, np.real(expect_floquet), "-")
axs[1].plot(tlist / T, np.real(expect_sesolve), "--")

axs[0].set_xlabel(r"$t \, / \, T$")
axs[1].set_xlabel(r"$t \, / \, T$")
axs[0].set_ylabel(r"Computational Time [$s$]")
axs[1].set_ylabel(r"$\langle \sigma_z \rangle$")
axs[0].legend(("Floquet", "sesolve"))

xticks = np.rint(np.linspace(0, N_T, N_T + 1, endpoint=True))
axs[0].set_xticks(xticks)
axs[0].set_xlim([0, N_T])
axs[1].set_xticks(xticks)
axs[1].set_xlim([0, N_T])

plt.show()
```

## One Dimensional Ising Chain

As a second use case for the Floquet method, we look at the one-dimensional Ising chain with a periodic drive.
We describe such a system using the Hamiltonian

$H(t) = g_0 \sum_{n=1}^N \sigma_z^{(n)} - J_0 \sum_{n=1}^{N - 1} \sigma_x^{(n)} \sigma_x^{(n+1)} + A \sin (\omega_d t) \sum_{n=1}^N \sigma_x^{(n)}$,

where $g_0$ is the level splitting, $J_0$ is the nearest-neighbour coupling constant and $A$ is the drive amplitude.

As outlined in the QuTiPv5 paper, it educational to study the dimensional scaling of the Floquet method compared to the standard `sesolve`.
Specifically how the crossing time (so when the Floquet method becomes faster) depends on the dimensions $N$.
Although we will not reproduce the whole plot, we implement the solution for one specific choice of $N$ and give the user the freedom to play with the paramteres.

```python
N = 4 # number of spins
g0 = 1 # energy-splitting
J0 = 1.4 # coupling strength
A = 1.0 # drive strength
omega = 1.0 * 2 * np.pi # drive frequency
T = 2 * np.pi / omega # drive period
```

```python
# For Hamiltonian Setup
def setup_Ising_drive(N, g0, J0, A, omega, data_type="CSR"):
"""
# N : number of spins
# g0 : splitting,
# J0 : couplings
# A : drive amplitude
# omega: drive frequency
"""
with CoreOptions(default_dtype=data_type):

sx_list, sy_list, sz_list = [], [], []
for i in range(N):
op_list = [qeye(2)] * N
op_list[i] = sigmax().to(data_type)
sx_list.append(tensor(op_list))
op_list[i] = sigmay().to(data_type)
sy_list.append(tensor(op_list))
op_list[i] = sigmaz().to(data_type)
sz_list.append(tensor(op_list))

# Hamiltonian - Energy splitting terms
H_0 = 0.0
for i in range(N):
H_0 += g0 * sz_list[i]

# Interaction terms
H_1 = qzero_like(H_0)
for n in range(N - 1):
H_1 += -J0 * sx_list[n] * sx_list[n + 1]

# Driving terms
if A > 0:
H_d = 0.0
for i in range(N):
H_d += A * sx_list[i]
args = {"w": omega}
H = [H_0, H_1, [H_d, lambda t, w: np.sin(w * t)]]
else:
args = {}
H = [H_0, H_1]

# Defining initial conditions
state_list = [basis(2, 1)] * (N - 1)
state_list.append(basis(2, 0))
psi0 = tensor(state_list)

# Defining expectation operator
e_ops = sz_list
return H, psi0, e_ops, args
```

```python
H, psi0, e_ops, args = setup_Ising_drive(N, g0, J0, A, omega)
```

```python
# Simulation parameters
N_T = 10
dt = 0.01
tlist = np.arange(0, N_T * T, dt)
tlist_0 = np.arange(0, T, dt) # One period tlist

options = {"progress_bar": False, "store_floquet_states": True}
```

```python
computational_time_floquet = np.ones(tlist.shape) * np.nan
computational_time_sesolve = np.ones(tlist.shape) * np.nan

expect_floquet = np.zeros((N, len(tlist)))
expect_sesolve = np.zeros((N, len(tlist)))
```

```python
# Running the simulation
for n, t in enumerate(tlist):
# Floquet basis
# --------------------------------
tic_f = time.perf_counter()
if t < T:
# find the floquet modes for the time-dependent hamiltonian
floquetbasis = FloquetBasis(H, T, args)
else:
floquetbasis = FloquetBasis(H, T, args, precompute=tlist_0)

# Decomposing inital state into Floquet modes
f_coeff = floquetbasis.to_floquet_basis(psi0)
# Obtain evolved state in the original basis
psi_t = floquetbasis.from_floquet_basis(f_coeff, t)
p_ex = expect(e_ops, psi_t)
toc_f = time.perf_counter()

# Saving data
computational_time_floquet[n] = toc_f - tic_f

# sesolve
# --------------------------------
tic_f = time.perf_counter()
output = sesolve(H, psi0, tlist[: n + 1], e_ops=e_ops, args=args)
p_ex_r = output.expect
toc_f = time.perf_counter()

# Saving data
computational_time_sesolve[n] = toc_f - tic_f

for i in range(N):
expect_floquet[i, n] = p_ex[i]
expect_sesolve[i, n] = p_ex_r[i][-1]
```

```python
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
ax = axs[0]
axs[0].plot(tlist / T, computational_time_floquet, "-")
axs[0].plot(tlist / T, computational_time_sesolve, "--")

axs[0].set_yscale("log")
axs[0].set_yticks(np.logspace(-3, -1, 3))
lg = []
for i in range(N):
axs[1].plot(tlist / T, np.real(expect_floquet[i, :]), "-")
lg.append(f"n={i+1}")
for i in range(N):
axs[1].plot(tlist / T, np.real(expect_sesolve[i, :]), "--", color="black")
lg.append("sesolve")

axs[0].set_xlabel(r"$t \, / \, T$")
axs[1].set_xlabel(r"$t \, / \, T$")
axs[0].set_ylabel(r"$Computational \; Time,\; [s]$")
axs[1].set_ylabel(r"$\langle \sigma_z^{{(n)}} \rangle$")
axs[0].legend(("Floquet", "sesolve"))
axs[1].legend(lg)
xticks = np.rint(np.linspace(0, N_T, N_T + 1, endpoint=True))
axs[0].set_xticks(xticks)
axs[0].set_xlim([0, N_T])
axs[1].set_xticks(xticks)
axs[1].set_xlim([0, N_T])
txt = f"$N={N}$, $g_0={g0}$, $J_0={J0}$, $A={A}$"
axs[0].text(0.0, 1.05, txt, transform=axs[0].transAxes)

plt.show()
```

## About

```python
about()
```

## Testing

Langhaarzombie marked this conversation as resolved.
Show resolved Hide resolved
```python
# TODO
```
Loading