Solving the Lindblad dynamics of a qubit chain#
In this tutorial we walk through an example of solving the time evolution of the density matrix of
coupled qubits using qiskit_dynamics
. The model that we solve consists of a time-independent
Hamiltonian for qubits interacting with their nearest neighbors along a ring. In addition, energy
relaxation terms acting on each qubit are modeled by using a non-Markovian master equation in
Lindblad form. We will discuss the system’s steady state as a function of the model parameters.
In the sections below we define a model, solve the dynamics and plot some observables using the following steps:
Define the number of qubits and precompute some matrix operators.
Define all relevant parameters and setup a
Solver
instance with the model of the system, consisting of the Hamiltonian and the jump operators of the Lindblad dissipator.Define the initial state and other parameters for the initial value problem, and evolve the system state.
Define observables and calculate their values as a function of time.
Plot some observables and discuss the results.
The model that we solve describes the time evolution of
The Hamiltonian is composed of a sum of single-qubit terms and two-qubit interactions,
We take the qubits to have identical parameters, with a diagonal term of frequency
where
We set the interactions between nearest-neighbor qubits to be of “flip-flop” type, also called “XY” coupling. We consider periodic boundary conditions, such that the last qubit is connected to the first qubit as well, with
In qiskit
, each qubit’s ground state is by convention the state
with
1. Prepare the single-qubit operators#
In qiskit-dynamics
, dynamical simulations are defined using the operators that act on the states
appearing in the differential equations. We start by creating the single-qubit operators that act on
each qubit in the simulation, and are represented as matrices in the Hilbert space of qiskit
library routines, it is easy to create Pauli
classes to indicate a qubit number, as below.
Below, we first set the number of qubits
import numpy as np
from qiskit.quantum_info import Operator, Pauli
N = 6
x_ops = []
y_ops = []
z_ops = []
qubits = range(N)
zeros = Operator(np.zeros((2 ** N, 2 ** N)))
for i in qubits:
X = zeros + Pauli('X')(i)
x_ops.append(X)
Y = zeros + Pauli('Y')(i)
y_ops.append(Y)
Z = zeros + Pauli('Z')(i)
z_ops.append(Z)
2. Setup the solver#
In this section we setup a Solver
class that stores and manipulates the model to be solved.
In the following, we will set Signal
class in this tutorial. See the other
tutorials for various generalizations of this approach supported with qiskit-dynamics
.
from qiskit_dynamics import Solver, Signal
nu_z = 4.
nu_x = 1.
J = 4.
Gamma = 4.
H = zeros
for i in qubits:
X = x_ops[i]
Z = z_ops[i]
H += .5 * 2 * np.pi * nu_x * X
H += .5 * 2 * np.pi * nu_z * Z
if N > 1:
j = i + 1 if i < (N - 1) else 0 # Nearest neighbors, with periodic boundary conditions
op = zeros + Pauli('XX')(i, j)
H += .5 * 2 * np.pi * J * op
op = zeros + Pauli('YY')(i, j)
H += .5 * 2 * np.pi * J * op
L_ops = []
L_sig = []
for i in qubits:
X = x_ops[i]
Y = y_ops[i]
L_ops.append(np.sqrt(Gamma) * 0.5 * (X + 1j * Y))
solver = Solver(static_hamiltonian=H, static_dissipators=L_ops)
3. Define the simulation parameters and solve the dynamics#
We now define the initial state for the simulation, the time span to simulate for, and the intermediate times for which the solution is requested.
from qiskit.quantum_info import DensityMatrix
t_final = 8. / Gamma
tau = .01
# A density matrix with all qubits in ground state
y0 = DensityMatrix.from_label('0' * N)
n_steps = int(np.ceil(t_final / tau)) + 1
t_eval = np.linspace(0., t_final, n_steps)
sol = solver.solve(t_span=[0., t_final], y0=y0, t_eval=t_eval)
4. Define the observables and calculate their values#
Below we calculate single-qubit Pauli expectation values for each qubit as a function of time (which define also the Bloch vector),
and also the mean components of the collective Bloch vector over all qubits, at each evaluation time,
Since both the model and the initial state as defined above are translation invariant (all qubits have identical parameters, and there is no boundary), we expect the solution to remain translation invariant as well. Hence the mean Bloch vector should be equal to any qubit’s Bloch vector, and observing that this equality holds is a simple and useful verification of the numerical solution that will be added in the next section.
n_times = len(sol.y)
x_data = np.zeros((N, n_times))
y_data = np.zeros((N, n_times))
z_data = np.zeros((N, n_times))
x_mean = np.zeros((n_times,))
y_mean = np.zeros((n_times,))
z_mean = np.zeros((n_times,))
for t_i, sol_t in enumerate(sol.y):
for qubit, obs in enumerate(x_ops):
x_data[qubit, t_i] = sol_t.expectation_value(obs).real
x_mean[t_i] = np.mean(x_data[:, t_i])
for qubit, obs in enumerate(y_ops):
y_data[qubit, t_i] = sol_t.expectation_value(obs).real
y_mean[t_i] = np.mean(y_data[:, t_i])
for qubit, obs in enumerate(z_ops):
z_data[qubit, t_i] = sol_t.expectation_value(obs).real
z_mean[t_i] = np.mean(z_data[:, t_i])
5. Plot some observables and discuss the results#
Finally, let’s plot some of the results of our dynamical simulation, using the single-qubit observables calculated as a function of time. We plot both the time evolution of the collective Bloch vector, and the Bloch vector at the final time, depicted in 3D within the Bloch sphere. We also print a warning if the Bloch vector at the final time is not translation invariant according to a simplified random check of two values (taken up to a small numerical precision).
Looking at the figures below, we see that for the above parameters the steady state is nearly pure,
with a large ground state component and a small tilt along the negative
from qiskit.visualization import plot_bloch_vector
import matplotlib.pyplot as plt
%matplotlib inline
fontsize = 16
_, ax = plt.subplots(figsize = (10, 6))
plt.rcParams.update({'font.size': fontsize})
plt.plot(t_eval, x_mean, label = '$ N^{-1}\sum_i \\langle X_i \\rangle$')
plt.plot(t_eval, y_mean, label = '$ N^{-1}\sum_i \\langle Y_i \\rangle$')
plt.plot(t_eval, z_mean, label = '$ N^{-1}\sum_i \\langle Z_i \\rangle$')
plt.legend(fontsize = fontsize)
ax.set_xlabel('$t$', fontsize = fontsize)
ax.set_title('Mean Bloch vector vs. $t$', fontsize = fontsize)
display(plot_bloch_vector([x_mean[-1], y_mean[-1], z_mean[-1]],
f'Mean Bloch vector at $t = {t_eval[-1]}$'))
if N > 1 and ((abs(x_mean[-1]) > 1e-5 and abs(x_data[0, -1] / x_mean[-1] - 1) > 1e-5 or
(abs(z_mean[-1]) > 1e-5 and abs(z_data[1, -1] / z_mean[-1] - 1) > 1e-5))):
print("The solution at the final time appears to break translation invariance. "
"The precision of the simulation should be examined.")

