Quasi-static scans

Characterizing the robustness of a pulse to quasi-static noise

The Q-CTRL Python Package enables you to evaluate the susceptibility of quantum controls to quasi-static noise on up to two simultaneous noise channels. This process can provide a useful characterization of the robustness of candidate controls. In this notebook we show how to compute quasi-static scans using the Q-CTRL Python Package.

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

# Predefined pulse imports
from qctrlopencontrols import new_predefined_driven_control

# Plotting imports
import matplotlib.pyplot as plt

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

Worked example: susceptibility to simultaneous amplitude and dephasing noise

In this example we will compare a series of composite $\pi$ pulses applied to a single qubit under amplitude and dephasing noise. The Hamiltonian of the quantum system is:

\begin{align*} H(t) = &\frac{1+\beta_\Omega(t)}{2}\left( \Omega(t) \sigma_- + \Omega^*(t) \sigma_+ \right) + \frac{\Delta(t)}{2}\sigma_z + \frac{\eta(t)}{2} \sigma_z \end{align*}

where $\Omega(t)$ is a time-dependent Rabi rate, $\beta_\Omega(t)$ is a fractional time-dependent amplitude fluctuation process, $\Delta(t)$ is a time-dependent clock shift, $\eta(t)$ is a small, slowly-varying stochastic dephasing noise process and $\sigma_k$ are the Pauli matrices.

We consider the following driven control schemes for the controllable $\Omega(t)$ and $\Delta(t)$ terms, which are available from Q-CTRL Open Controls and described in the Q-CTRL technical documentation:

  • primitive,
  • BB1,
  • SK1,
  • CORPSE.

In a 2D quasi-static scan, we scan across a two-dimensional grid of values $(\beta_\Omega,\eta)$ for $\beta_\Omega(t)\equiv \beta_\Omega$ and $\eta(t)\equiv\eta$, and calculate the infidelity of the resulting gate in each case. Comparing the variation in infidelity across the grid gives information about the robustness of the appropriate control to simultaneous quasi-static noise on the two channels.

In this particular system we use a grid with $\beta_\Omega\in [-0.5,0.5]$ and $\eta\in[-\Omega_{\texttt{max}}, \Omega_{\texttt{max}}]$ (where $\Omega_{\texttt{max}}$ is the maximum Rabi rate).

Creating the system, controls and pulses

As described in the Setup feature guide, we first set up Python objects representing the system, controls and pulses.

# 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_x = np.array([[0., 1.],[1., 0.]], dtype=np.complex)
sigma_m = np.array([[0., 0.],[1., 0.]], dtype=np.complex)

# Define control parameters
omega_max = 2*np.pi *  1e6 #Hz
total_rotation = np.pi

# Define schemes for driven controls to compare 
schemes = {scheme: {} for scheme in ['primitive', 'BB1', 'SK1', 'CORPSE']}

for scheme in schemes:
    # Define system object
    system = qctrl.factories.systems.create(
        name=scheme,
        hilbert_space_dimension=2,
    )

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

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

    # Define pulse objects using pulses from Q-CTRL Open Controls
    pulse = new_predefined_driven_control(
        rabi_rotation=total_rotation,
        azimuthal_angle=0.,
        maximum_rabi_rate=omega_max,
        scheme=scheme,
        name=scheme,
    )

    drive_pulse = qctrl.factories.custom_pulses.create(
        control=drive,
        segments=[{'duration': d, 'value': v} 
                  for d, v in zip(pulse.durations,
                                  pulse.rabi_rates * np.exp(1j*pulse.azimuthal_angles))],
    )

    shift_pulse = qctrl.factories.custom_pulses.create(
        control=shift,
        segments=[{'duration': d, 'value': v} for d, v in zip(pulse.durations, pulse.detunings)],
    )

    # Define noises

    amplitude_noise = qctrl.factories.control_noises.create(
        name='Amplitude',
        control=drive,
    )

    dephasing_noise = qctrl.factories.additive_noises.create(
        name='Dephasing',
        operator=sigma_z/2.,
        system=system,
    )

    # Save relevant quantities for later use
    schemes[scheme]['system'] = system
    schemes[scheme]['amplitude_noise'] = amplitude_noise
    schemes[scheme]['dephasing_noise'] = dephasing_noise

Creating the target

Using a quasi-static scan to evaluate susceptibility to noise requires a target operation, which in this example is a Pauli-X gate. This target is represented as a target object, which requires a system, unitary operator and projection operator (which describes the subspace of interest, in this case the full Hilbert space).

for scheme in schemes:
    target = qctrl.factories.targets.create(
        system=schemes[scheme]['system'],
        unitary_operator=sigma_x,
        projection_operator=identity,
    )

Creating the quasi-static function

Computation of a quasi-static scan is represented as a quasi_static_function object, which requires either one or two noise objects together with a corresponding array of coefficients (in this example, these coefficients correspond to values for $\beta_\Omega$ and $\eta$). Depending on whether one or two noises are provided, a one- or two-dimensional scan will be produced.

# Define coefficient arrays
dephasing_coefficients = np.linspace(-1., 1., 101) * omega_max
amplitude_coefficients = np.linspace(-0.5, 0.5, 51)

# Define quasi-static function objects
for scheme in schemes:
    schemes[scheme]['quasi_static_function'] = qctrl.factories.quasi_static_functions.create(
        x_noise=schemes[scheme]['dephasing_noise'],
        x_coefficients=dephasing_coefficients,
        y_noise=schemes[scheme]['amplitude_noise'],
        y_coefficients=amplitude_coefficients,
    )

Calculating the quasi-static function

With the quasi_static_function object prepared, all that remains is to perform the actual computation. This may be accomplished using the quasi_static_functions service, as shown below.

for scheme in schemes:
    schemes[scheme]['result'] = qctrl.services.quasi_static_functions.calculate(
        schemes[scheme]['quasi_static_function']
    )

Extracting the infidelities

The infidelities calculated for the quasi-static scan are available in the sampled_points field of the returned quasi_static_function object. To unpack these infidelities, we may iterate through the sample points, saving the infidelity associated with each pair of noise coefficients.

for scheme in schemes:
    infidelities = np.empty((len(dephasing_coefficients), len(amplitude_coefficients)))
    for sampled_point in schemes[scheme]['result'].sampled_points:
        dephasing_index = np.where(np.isclose(dephasing_coefficients, sampled_point['coefficients'][0]))
        amplitude_index = np.where(np.isclose(amplitude_coefficients, sampled_point['coefficients'][1]))
                                   
        infidelities[dephasing_index, amplitude_index] = sampled_point['infidelity']

    schemes[scheme]['infidelities'] = infidelities

Visualizing the susceptibility to quasi-static noise

For 2D scans, density plots can provide intuitive visualizations of the noise robustness. With the grid of infidelities extracted above, it is simple to create such plots using the Matplotlib library.

X, Y = np.meshgrid(dephasing_coefficients/omega_max, amplitude_coefficients, indexing='ij')

for scheme in schemes:
    Z = schemes[scheme]['infidelities']

    fig, ax = plt.subplots()
    fig.set_figheight(4)
    fig.set_figwidth(10)
    contours = plt.contour(X, Y, Z, levels = [.001,.01,.1,.5], colors='#680CEA')
    plt.clabel(contours, inline=True, fontsize=8)

    cmap_reversed = plt.cm.get_cmap('gray').reversed()

    plt.imshow(Z.T, extent=[np.min(dephasing_coefficients)/omega_max, np.max(dephasing_coefficients)/omega_max,
                            np.min(amplitude_coefficients), np.max(amplitude_coefficients),], 
               origin='lower',
               cmap=cmap_reversed, alpha=0.8,
              vmin=0, vmax=1)
    
    cbar = plt.colorbar(pad=0.08)
    #cbar.ax.set_yticklabels(['0','1','2','>3'])
    cbar.set_label('Pulse infidelity', labelpad=-50)
    plt.title('Noise susceptibility of '+scheme+' pulse' )
    plt.ylabel(r'Amplitude coefficient $\beta_\Omega$')
    plt.xlabel(r'Relative dephasing coefficient $\eta/\Omega_{max}$')
    plt.show()

Summary

The plots demonstrate clearly the different noise susceptibilities of the different controls. For example, the BB1 control exhibits excellent robustness to amplitude noise (it retains a low infidelity across a wide range of amplitude noise values), but low dephasing robustness (infidelity increases rapidly as the dephasing coefficient varies from zero). The CORPSE control is the opposite: it is robust to dephasing noise, but highly susceptible to amplitude noise.

We have thus demonstrated how the Q-CTRL Python Package can be used to characterize the robustness of different controls to quasi-static noise on two simultaneous noise channels.

Example: susceptibility to dephasing noise

The Q-CTRL Python Package may also be used to perform quasi-static scans across a single noise channel. In this example we consider the simple case of a $\pi/2$ pulse performed on a single driven qubit experiencing dephasing noise. The system is described by the Hamiltonian:

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

where $\Omega(t)$ is a time-dependent Rabi rate, $\Delta(t)$ is a time-dependent clock shift, $\eta(t)$ is a small, slowly-varying stochastic dephasing noise process and $\sigma_k$ are the Pauli matrices.

In this case we consider only two driven controls, primitive and CORPSE.

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

# Define control parameters
omega_max = 2*np.pi *  1e6 #Hz
total_rotation = np.pi/2

# Define coefficient array
dephasing_coefficients = np.linspace(-1., 1., 101) * omega_max

# For each scheme, compute and plot the results of the quasi-static scan
for scheme in ['primitive', 'CORPSE']:
    # Define system object
    system = qctrl.factories.systems.create(
        name=scheme,
        hilbert_space_dimension=2,
    )

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

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

    # Define pulse objects using pulses from Q-CTRL Open Controls
    pulse = new_predefined_driven_control(
        rabi_rotation=total_rotation,
        azimuthal_angle=0.,
        maximum_rabi_rate=omega_max,
        scheme=scheme,
        name=scheme,
    )

    drive_pulse = qctrl.factories.custom_pulses.create(
        control=drive,
        segments=[{'duration': d, 'value': v} 
                  for d, v in zip(pulse.durations,
                                  pulse.rabi_rates * np.exp(1j*pulse.azimuthal_angles))],
    )

    shift_pulse = qctrl.factories.custom_pulses.create(
        control=shift,
        segments=[{'duration': d, 'value': v} for d, v in zip(pulse.durations, pulse.detunings)],
    )

    # Define noise
    dephasing_noise = qctrl.factories.additive_noises.create(
        name='Dephasing',
        operator=sigma_z/2.,
        system=system,
    )
    
    # Define target
    target = qctrl.factories.targets.create(
        system=system,
        unitary_operator=sqrt_sigma_x,
        projection_operator=identity,
    )
    
    # Define quasi-static function
    quasi_static_function = qctrl.factories.quasi_static_functions.create(
        x_noise=dephasing_noise,
        x_coefficients=dephasing_coefficients,
    )
    
    # Compute quasi-static function
    result = qctrl.services.quasi_static_functions.calculate(
        quasi_static_function,
    )

    # Extract infidelities
    noises_and_infidelities = np.array(
        [[sampled_point['coefficients'][0], sampled_point['infidelity']]
         for sampled_point in result.sampled_points])
    
    noises_and_infidelities_sorted = noises_and_infidelities[
        noises_and_infidelities.argsort(axis=0)[:, 0]]

    # Plot infidelities
    plt.plot(noises_and_infidelities_sorted[:, 0]/omega_max,
             noises_and_infidelities_sorted[:, 1],
             label=scheme)

plt.title('Dephasing noise susceptibility of different pulses' )
plt.ylabel('Infidelity')
plt.xlabel(r'Relative dephasing coefficient $\eta/\Omega_{max}$')
plt.legend()
plt.show()

Wiki

Comprehensive knowledge base of quantum control theory

Explore