Setup

Setting up a quantum system

In order to use the Q-CTRL Python Package to analyze and improve your quantum system, you must first translate your system into the language used by the Q-CTRL ecosystem. In this notebook we demonstrate the most common aspects of this translation process.

Imports and initialization

All usage of the Q-CTRL Python Package begins by importing the qctrl package and starting a session.

# Essential imports
import numpy as np
from qctrl import Qctrl

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

Worked example: two-qubit system with custom pulses

To demonstrate the procedure, we present a detailed worked example. Specifically, we consider a two-qubit system represented by the following Hamiltonian:

\begin{align*} H(t) = & \frac{\nu_a}{2} \sigma_{z,a} + \frac{\nu_b}{2} \sigma_{z,b} + \frac{\Omega_a(t)}{2} \sigma_{-,a} + \frac{\Omega^*_a(t)}{2} \sigma_{+,a} + \frac{\Omega_b(t)}{2} \sigma_{-,b} + \frac{\Omega^*_b(t)}{2} \sigma_{+,b}\\ & + \frac{\Lambda(t)}{2} \sigma_{-,a} \sigma_{+,b} + \frac{\Lambda^*(t)}{2} \sigma_{+,a} \sigma_{-,b} + \frac{\Delta_a(t)}{2} \sigma_{z,a} + \frac{\Delta_b(t)}{2} \sigma_{z,b} + \eta_a(t) \sigma_{z,a} + \eta_b(t)\sigma_{z,b}, \end{align*}

where $\nu_k$ are the qubit detunings, $\Omega_k(t)$ are time-dependent Rabi rates (for example created by a microwave drive), $\Lambda(t)$ is a time-dependent coupling between the qubits, $\Delta_k(t)$ are time-dependent clock shifts, $\eta_k(t)$ are small, slowly-varying, stochastic dephasing noise processes and $\sigma_{\{+,-,z\}, k}$ are the Pauli matrices.

We consider custom pulses defined by:

\begin{align*} \Omega_a(t) &= \begin{cases} 2\pi\times 1.5e^{0.5\pi i}\times 10^{6} & 0\leq t\leq 2\times 10^{-6}\\ 0 & 2\times 10^{-6}\leq t\leq 4\times 10^{-6}\\ 2\pi\times 0.5e^{-0.5\pi i}\times 10^{6} & 4\times10^{-6}\leq t\leq 6\times 10^{-6} \end{cases}\\ \Omega_b(t) &= \begin{cases} 2\pi\times 1.0e^{0.25\pi i}\times 10^{6} & 0\leq t\leq 2\times 10^{-6}\\ 0 & 2\times 10^{-6}\leq t\leq 4\times 10^{-6}\\ 2\pi\times 0.5e^{1.25\pi i}\times 10^{6} & 4\times10^{-6}\leq t\leq 6\times 10^{-6} \end{cases}\\ \Lambda(t) &= \begin{cases} 0 & 0\leq t\leq 2\times 10^{-6}\\ 2\pi\times 1.0e^{\pi i}\times 10^{6} & 2\times 10^{-6}\leq t\leq 4\times 10^{-6}\\ 0 & 4\times10^{-6}\leq t\leq 6\times 10^{-6} \end{cases}\\ \Delta_a(t) &= \begin{cases} 2\pi\times 10.0\times 10^{6} & 0\leq t\leq 2\times 10^{-6}\\ 0 & 2\times 10^{-6}\leq t\leq 4\times 10^{-6}\\ 2\pi\times -10.0\times 10^{6} & 4\times10^{-6}\leq t\leq 6\times 10^{-6} \end{cases}\\ \Delta_b(t) &= \begin{cases} 2\pi\times -1.0\times 10^{6} & 0\leq t\leq 2\times 10^{-6}\\ 0 & 2\times 10^{-6}\leq t\leq 4\times 10^{-6}\\ 2\pi\times 1.5\times 10^{6} & 4\times10^{-6}\leq t\leq 6\times 10^{-6} \end{cases} \end{align*}

Such a setup would be useful for performing simulation or characterizing susceptibility to noise via filter functions.

Interpreting a quantum Hamiltonian as a Q-CTRL system

The first step in analyzing your system using the Q-CTRL Python Package is to split the Hamiltonian of your quantum system into a sum of controls and noises, as we describe below.

Following the Q-CTRL convention, the two-qubit Hamiltonian above is represented by six controls:

  • a drift control with operator $\nu_a \sigma_{z,a}/2 + \nu_b \sigma_{z,b}/2$,
  • a drive control with operator $\sigma_{-,a}/2$ and complex pulse $\Omega_a(t)$,
  • a drive control with operator $\sigma_{-,b}/2$ and complex pulse $\Omega_b(t)$,
  • a drive control with operator $\sigma_{-,a} \sigma_{+,b}/2$ and complex pulse $\Lambda(t)$,
  • a shift control with operator $\sigma_{z,a}/2$ and real pulse $\Delta_a(t)$,
  • a shift control with operator $\sigma_{z,b}/2$ and real pulse $\Delta_b(t)$,

and two noises:

  • an additive noise with operator $\sigma_{z,a}$,
  • an additive noise with operator $\sigma_{z,b}$.

With this decomposition in hand, we may proceed to set up the appropriate Python objects.

Creating the system

The main object representing your quantum system in the Q-CTRL ecosystem is the system. Creating a bare system is as simple as providing a name and Hilbert space dimension.

system = qctrl.factories.systems.create(
    name="Two-qubit with dephasing",
    hilbert_space_dimension=4,
)

Creating the controls

Each control in your quantum system is represented as a control object. Controls may be of type drive, shift or drift. Each control requires a name, system and operator.

# Define standard matrices
identity = np.array([[1., 0.],[0., 1.]], dtype=np.complex)
sigma_z = np.array([[1., 0.],[0., -1.]], dtype=np.complex)
sigma_m = np.array([[0., 0.],[1., 0.]], dtype=np.complex)

# Define physical constants
nu_a = 2*np.pi * 0.5 * 1e6 #Hz
nu_b = 2*np.pi * 0.5 * 1e6 #Hz

# Define control objects
drift = qctrl.factories.drift_controls.create(
    name='Detunings',
    system=system,
    operator=nu_a*np.kron(sigma_z,identity)/2 + nu_b*np.kron(sigma_z,identity)/2,
)

drive_A = qctrl.factories.drive_controls.create(
    name='Rabi rate A',
    system=system,
    operator=np.kron(sigma_m,identity)/2,
)

drive_B = qctrl.factories.drive_controls.create(
    name='Rabi rate B',
    system=system,
    operator=np.kron(identity,sigma_m)/2,
)

coupling = qctrl.factories.drive_controls.create(
    name='Coupling',
    system=system,
    operator=np.kron(sigma_m,sigma_m.T)/2,
)

clock_A = qctrl.factories.shift_controls.create(
    name='Clock A',
    system=system,
    operator=np.kron(sigma_z,identity)/2,
)

clock_B = qctrl.factories.shift_controls.create(
    name='Clock B',
    system=system,
    operator=np.kron(identity,sigma_z)/2,
)

Creating the noises

Each noise in your quantum system is represented as a noise object. In this example we have only additive noises, which are represented as additive noise objects and require a name, system and operator. In other cases you may need to consider control noises, which are represented as control noise objects and require only a name and control.

dephasing_A = qctrl.factories.additive_noises.create(
    name='Dephasing A',
    system=system,
    operator=np.kron(sigma_z,identity),
)

dephasing_B = qctrl.factories.additive_noises.create(
    name='Dephasing B',
    system=system,
    operator=np.kron(identity,sigma_z),
)

Creating the pulses

Each drive and shift control in your system must have an associated pulse, which describes the scalar-valued function of time that modulates the control operator. Each pulse is represented as a pulse object. Depending on your application, you will use pulse objects of different types that contain different data. For example, if performing a control optimization, you will use optimum pulses that will be tuned in order to maximize system performance. Alternatively, if calculating a filter function or performing a simulation using custom controls, you will use custom pulses that describe explicit piecewise-constant functions of time.

In this case we show how to create basic custom pulses corresponding to the functions of time described at the start of this section, each of which requires a control and segments.

# Define physical constants
segment_duration = 1 * 1e-6 #s

# Define pulse objects
drive_A_pulse = qctrl.factories.custom_pulses.create(
    control=drive_A,
    segments=[
        {'duration': segment_duration, 'value': 2*np.pi * 1.5*np.exp(0.5j*np.pi) * 1e6},
        {'duration': segment_duration, 'value': 0},
        {'duration': segment_duration, 'value': 2*np.pi * 0.5*np.exp(-0.5*np.pi) * 1e6},
    ],
)

drive_B_pulse = qctrl.factories.custom_pulses.create(
    control=drive_B,
    segments=[
        {'duration': segment_duration, 'value': 2*np.pi * 1.0*np.exp(0.25j*np.pi) * 1e6},
        {'duration': segment_duration, 'value': 0},
        {'duration': segment_duration, 'value': 2*np.pi * 1.0*np.exp(1.25j*np.pi) * 1e6},
    ],
)

coupling_pulse = qctrl.factories.custom_pulses.create(
    control=coupling,
    segments=[
        {'duration': segment_duration, 'value': 0},
        {'duration': segment_duration, 'value': 2*np.pi * 1.0*np.exp(1.0j*np.pi) * 1e6},
        {'duration': segment_duration, 'value': 0},
    ],
)

clock_A_pulse = qctrl.factories.custom_pulses.create(
    control=clock_A,
    segments=[
        {'duration': segment_duration, 'value': 2*np.pi * 10.0 * 1e6},
        {'duration': segment_duration, 'value': 0},
        {'duration': segment_duration, 'value': 2*np.pi * -10.0 * 1e6},
    ],
)

clock_B_pulse = qctrl.factories.custom_pulses.create(
    control=clock_B,
    segments=[
        {'duration': segment_duration, 'value': 2*np.pi * -1.0 * 1e6},
        {'duration': segment_duration, 'value': 0},
        {'duration': segment_duration, 'value': 2*np.pi * 1.5 * 1e6},
    ],
)

Summary

With control, noise and pulse objects set up, the system can now be used to perform a variety of useful analyses, as demonstrated in the other feature guides.

Example: one-qubit system with optimum pulses

Next we present a full example of a one-qubit system set up for optimization. We consider the Hamiltonian:

\begin{align*} H(t) = & \frac{\nu}{2} \sigma_z + \frac{\Omega(t)}{2} \sigma_- + \frac{\Omega^*(t)}{2} \sigma_+ + \frac{\Delta(t)}{2} \sigma_z, \end{align*}

where $\nu$ is the qubit detuning, $\Omega(t)$ is a time-dependent Rabi rate, $\Delta(t)$ is a time-dependent clock shift, and $\sigma_k$ are the Pauli matrices.

In this case the functions of time $\Omega(t)$ and $\Delta(t)$ are not fixed, and instead may be optimized by the Q-CTRL optimization engine in order to achieve some target operation.

Below we show how to set up the system, control, and pulse objects for this system.

system = qctrl.factories.systems.create(
    name="One-qubit with dephasing",
    hilbert_space_dimension=2,
)

# Define physical constants
nu = 2*np.pi * 0.5 * 1e6 #Hz
omega_max = 2*np.pi * 0.5e6 #Hz
delta_max = 2*np.pi * 0.5e6 #Hz
segment_count = 5
duration = 10e-6 #s

# Define control objects
drift = qctrl.factories.drift_controls.create(
    name='Detuning',
    system=system,
    operator=nu*sigma_z/2,
)

drive = qctrl.factories.drive_controls.create(
    name='Rabi rate',
    system=system,
    operator=sigma_m/2,
)

clock = qctrl.factories.shift_controls.create(
    name='Clock',
    system=system,
    operator=sigma_z/2,
)

# Define pulse objects
drive_pulse = qctrl.factories.optimum_pulses.create(
    control=drive,
    upper_bound=omega_max,
    fixed_modulus=False,
    segment_count=segment_count,
    duration=duration,
)

shift_pulse = qctrl.factories.optimum_pulses.create(
    control=clock,
    upper_bound=delta_max,
    fixed_modulus=False,
    segment_count=segment_count,
    duration=duration,
)

Wiki

Comprehensive knowledge base of quantum control theory

Explore