# How to simulate open system dynamics

Calculating the dynamics of a quantum system described by a GKS–Lindblad master equation

The Q-CTRL Python package enables you to simulate not only the evolution of isolated quantum systems with and without noise, but also the evolution of systems that are interacting with their environment. You can express the dynamics of these open quantum systems in terms of a master equation, such as the one described by Gorini, Kossakowski, and Sudarshan (GKS) and Lindblad:

$$\frac{\mathrm{d}}{\mathrm{d} t} \rho(t) = - i \left[ H_{\rm s} (t), \rho(t) \right] + \gamma L \rho(t) L^\dagger - \frac{1}{2} \gamma \rho(t) L^\dagger L - \frac{1}{2} \gamma L^\dagger L \rho(t).$$

The tools described in this notebook allow you to solve this GKS–Lindblad master equation to obtain the time evolution for the system that you want.

## Summary workflow: Open system simulations with graphs

### 1. Define Lindblad terms in computational graph

To describe the dynamics of an open system with the Q-CTRL Python package, you start by setting up a graph object as described in the How to represent quantum systems using graphs user guide. The difference is that besides setting up a graph with a piecewise-constant system Hamiltonian $H_{\rm s} (t)$ for the controls, you must also define the Lindblad terms that appear in the GKS–Lindblad master equation above.

### 2. Calculate density matrix evolution

If you provide a tuple with the rate $\gamma$ and Lindblad operator $L$ as lindblad_terms to the graph operation graph.density_matrix_evolution_pwc, together with a piecewise constant hamiltonian and an initial_density_matrix, you obtain the value of the density matrix $\rho(t)$ for the sample_times that you requested in a call of the function qctrl.functions.calculate_graph. It also performs the same operation for a closed system (which takes into account the system Hamiltonian, but not the Lindbladian), so you can compare the two types of evolution.

Note that the density_matrix_evolution_pwc function can be inefficient when used with the default parameters if the dimension of your Hilbert space is more than roughly $10$. A separate user guide covers efficient approaches for large systems.

## Worked example: Simulating a single-qubit open system

The master equation integration tool from the Q-CTRL Python package allows you to obtain the solution of a GKS–Lindblad equation. This equation describes the evolution of a system that obeys a system Hamiltonian $H_{\rm s}$ and also any number of extra Lindbladian terms consisting of a rate $\gamma_i$ and a Lindblad operator $L_i$:

$$\frac{\mathrm{d} \rho(t)}{\mathrm{d} t} = - i \left[ H_{\rm s}(t), \rho(t) \right] + \sum_i \gamma_i \left[ L_i \rho(t) L_i^\dagger - \frac{1}{2} \rho(t) L_i^\dagger L_i - \frac{1}{2} L_i^\dagger L_i \rho(t) \right].$$

For this example, consider a single-qubit system whose evolution you can control through a complex Rabi rate $\Omega(t)$ and a detuning $\Delta(t)$, so that the system Hamiltonian is:

$$H_{\rm s}(t) = \frac{1}{2} \Omega(t) \sigma_- + \frac{1}{2} \Omega^* (t) \sigma_+ + \Delta(t) \sigma_z,$$

where $\sigma_x$, $\sigma_y$, and $\sigma_z$ are the Pauli matrices and $\sigma_\pm = (\sigma_x \mp i \sigma_y)/2$.

Suppose this system is also subject to $T_2$ noise, which you can represent by the Lindblad operator $L= \sigma_z$ with associated rate $\gamma = 1/(2T_2)$. By using the tools from the Q-CTRL Python package to solve this master equation, you can obtain the evolution of the system in terms of $\rho(t)$.

The first step is to set up the Python object that represents the pulse, which you can define by yourself or by importing one of the predefined pulses in the Q-CTRL Open Controls package. In particular, this example illustrates the case where the target operation is an $X_{\pi}$ rotation applied to a system initially at the state $\left|0\right\rangle$, which corresponds to the initial density matrix $\rho(0) = \left| 0 \right\rangle \left\langle 0 \right|$. Such an operation should flip the qubit from the state $\left|0\right\rangle$ to the state $\left|1\right\rangle$.

import matplotlib.pyplot as plt
import numpy as np
from qctrlopencontrols import new_primitive_control
from qctrlvisualizer import get_qctrl_style

from qctrl import Qctrl

plt.style.use(get_qctrl_style())

# Start a session with the API
qctrl = Qctrl()

# Define standard matrices.
identity = np.identity(2, dtype=complex)
sigma_z = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex)
sigma_m = np.array([[0.0, 1.0], [0.0, 0.0]], dtype=complex)

# Define control parameters.
omega_max = 2 * np.pi * 1e6  # Hz
total_rotation = np.pi
initial_state = np.array([[1], [0]], dtype=complex)
initial_density_matrix = np.kron(initial_state, initial_state.T.conj())

# Obtain predefined pulse from Q-CTRL Open Controls.
predefined_pulse = new_primitive_control(
rabi_rotation=total_rotation, azimuthal_angle=0.0, maximum_rabi_rate=omega_max
)


### Defining the simulation graph

The code block below illustrates how to obtain the value of the density matrix $\rho(t)$ for the sample_times that you requested in a call of the function qctrl.functions.calculate_graph.

# Define time parameters of the simulation.
duration = sum(predefined_pulse.durations)
sample_times = np.linspace(0, duration, 100)

# Create simulation graph.
graph = qctrl.create_graph()

# Define the controls using the predefined pulse.
omega = graph.pwc(
values=predefined_pulse.rabi_rates * np.exp(1j * predefined_pulse.azimuthal_angles),
durations=predefined_pulse.durations,
)
delta = graph.pwc(
values=predefined_pulse.detunings, durations=predefined_pulse.durations
)

# Define terms of the Hamiltonian.
sigma_x_term = graph.pwc_operator_hermitian_part(omega * sigma_m)
sigma_z_term = delta * sigma_z
hamiltonian = sigma_x_term + sigma_z_term

# Define Lindblad term of the master equation.
T2 = 1e-6  # s

# Calculate density matrix evolution according to the master equation.
graph.density_matrix_evolution_pwc(
initial_density_matrix=initial_density_matrix,
hamiltonian=hamiltonian,
sample_times=sample_times,
name="open_system_states",
)

# Calculate vector state evolution for a closed system.
graph.matmul(
graph.time_evolution_operators_pwc(
hamiltonian=hamiltonian, sample_times=sample_times
),
initial_state,
name="closed_system_states",
)

# Run simulation.
results = qctrl.functions.calculate_graph(
graph=graph, output_node_names=["closed_system_states", "open_system_states"]
)

Your task calculate_graph (action_id="618143") has completed.



### Plotting the system evolution

After you obtain the evolution of the system with and without $T_2$ noise, you can plot the results at each of the sample_times that you requested to see how the system goes from the initial state $\left|0\right\rangle$ to the target state $\left| 1 \right\rangle$. While the evolution for a closed system leads perfectly to the desired final state, the open system evolution is disrupted by the presence of $T_2$ noise.

fig, axs = plt.subplots(1, 2, figsize=(15, 5))
fig.suptitle("Population dynamics", y=1.1)

for k in range(2):
axs[k].set_xlabel(f"Time (µs)")
axs[k].set_ylabel(rf"Population of $|{k}\rangle$")
axs[k].plot(
sample_times * 1e6,
np.abs(results.output["closed_system_states"]["value"][:, k, 0]) ** 2,
label="Closed system",
)
axs[k].plot(
sample_times * 1e6,
np.abs(results.output["open_system_states"]["value"][:, k, k]),
label=f"Open system with $T_2$ = {T2*1e6} μs",
)

hs, ls = axs[0].get_legend_handles_labels()
fig.legend(handles=hs, labels=ls, loc="center", bbox_to_anchor=(0.5, 1.0), ncol=2)

plt.show()


### Summary

The open-system tools from the Q-CTRL Python package allow you to use the graph framework to calculate more general types of system evolution. While the tools described in this user guide show how you can obtain the evolution of a system that follows the Schrödinger equation, the open system tools allow you to solve a more general master equation. You can also use these tools in optimization graphs, if you have a cost function that represents what you want to optimize.