Performance metrics

Quantifying the performance of quantum controls

The Q-CTRL Python Package allows you to calculate metrics that quantify the performance of your quantum controls. In this guide we show how to calculate the infidelities with respect to different kinds of noise, or without noise.

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
from qctrlvisualizer import plot_controls, plot_filter_functions

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

Worked example: performance of a single qubit undergoing amplitude and dephasing noise

We will calculate the performance metrics of a single qubit undergoing amplitude and dephasing noise. The Hamiltonian of the quantum system is:

$$ H(t) = \frac{1+\beta_\gamma(t)}{2}\left( \gamma(t) \sigma_- + \gamma^*(t) \sigma_+ \right) + \frac{\Delta(t)}{2}\sigma_z + \frac{\eta(t)}{2} \sigma_z $$

where $\gamma(t)$ is a time-dependent Rabi rate created by a microwave drive, $\beta_\gamma(t)$ is a fractional time-dependent amplitude fluctuation process, $\Delta(t)$ is the time-dependent frequency shift, $\eta(t)$ is a small stochastic slowly-varying dephasing noise process, and $\sigma_k$ are Pauli matrices (with $\sigma_\pm = \frac{\sigma_x \pm i \sigma_y}{2}$).

We will test the following controls in this single-qubit system:

  • primitive,
  • SK1,
  • CORPSE.

These schemes are available from Q-CTRL Open Controls and described in the Q-CTRL technical documentation.

We will use the tools available in the Q-CTRL Python Package to calculate the infidelity of these pulses. When we calculate the filter functions for each control solution and noise operator, noise spectral densities will be ingested to calculate the associated filter function infidelity.

Creating system, controls, pulses, and targets

As described in the Setup feature guide, we first set up Python objects representing the system, controls, and pulses. We also set up a target operation for the system, a procedure that is described in more detail in the Optimization feature guide.

# Define standard matrices
identity = np.array([[1., 0.], [0., 1.]], dtype=np.complex)
sigma_x = np.array([[0., 1.], [1., 0.]], 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)

# Defining target operation
total_rotation = np.pi/4.
azimuthal_angle = 0
target_unitary = np.cos(total_rotation/2.)*identity - 1.j*np.sin(total_rotation/2.)*sigma_x

# Define control parameters
omega_max = 2 * np.pi * 0.1e9  # Hz
delta_max = 2 * np.pi * 0.1e9  # Hz

open_control_schemes = ['primitive', 'SK1', 'CORPSE']

# Keeping track of created systems for later reference
schemes = {scheme: {} for scheme in open_control_schemes}

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

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

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

    )
    
    # Define pulse objects using pulses from Q-CTRL Open Controls
    pulse = new_predefined_driven_control(
        rabi_rotation=total_rotation,
        azimuthal_angle=azimuthal_angle,
        maximum_rabi_rate=omega_max,
        scheme=scheme,
        name=scheme
    )
    
    drive_segments = [{'duration': d, 'value': v}
                      for d, v in zip(pulse.durations, pulse.rabi_rates * np.exp(+1j*pulse.azimuthal_angles))]
    
    qctrl.factories.custom_pulses.create(
        control=drive,
        segments=drive_segments
    )

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

    # Define noises
    amplitude_noise = qctrl.factories.control_noises.create(
        name='Amplitude noise',
        control=drive
    )

    dephasing_noise = qctrl.factories.additive_noises.create(
        name='Dephasing',
        operator=sigma_z/2.,
        system=system
    )
    
    # Define target
    target = qctrl.factories.targets.create(
        unitary_operator=target_unitary,
        projection_operator=identity,
        system=system,
    )
    
    # Save relevant objects for later use
    scheme_objects['system'] = system
    scheme_objects['amplitude_noise'] = amplitude_noise
    scheme_objects['dephasing_noise'] = dephasing_noise
    scheme_objects['target'] = target

Defining the filter functions

The Q-CTRL Python Package allows the calculation of filter functions, which characterize the sensitivity of a control to noise as a function of the noise frequency. More details about how to create them can be found in the Filter functions feature guide.

# Define filter function parameters
sample_count = 3000
interpolated_amplitudes = np.logspace(-1, np.log10(omega_max), 1000)  # Hz
interpolated_detuning_frequencies = np.logspace(-2, np.log10(delta_max), 1000)  # Hz

for scheme_objects in schemes.values():
    # Define filter functions
    filter_function_amplitude = qctrl.factories.filter_functions.create(
        noise=scheme_objects['amplitude_noise'],
        sample_count=sample_count,
        interpolated_frequencies=interpolated_amplitudes,
    )

    filter_function_dephasing = qctrl.factories.filter_functions.create(
        noise=scheme_objects['dephasing_noise'],
        sample_count=sample_count,
        interpolated_frequencies=interpolated_detuning_frequencies,
    )
    
    # Save filter functions
    scheme_objects['filter_function_amplitude'] = filter_function_amplitude
    scheme_objects['filter_function_dephasing'] = filter_function_dephasing

Defining noise spectral densities

As explained in the technical documentation for filter functions, the integral of these functions multiplied by a noise spectral density can be used to estimate the infidelity of a system subject to noise. We already defined the filter functions in the previous section, so the next step we need to take is to define spectral densities for the noise. In the examples below, we do this with the qctrl.factories.piecewise_power_law_noise_spectral_densities.create() method.

This method creates a distribution that consists of a series of segments in which the function varies according to some power of the frequency. It takes the following arguments:

  • noise: The kind of noise that this spectral density represents.
  • interpolated_frequencies: List of frequencies in which the spectral density will be calculated.
  • frequencies: The list of the limits of each of the segments.
  • coefficients: A list of the coefficients that multiply each of the segments.
  • exponents: A list of the exponents of the frequency in each of the segments.

In other words, the spectral density generated by this piecewise power law is: $$ S(f) = \sum_n C_n f^{e_n} H(f-f_n) H(f_{n+1}-f), $$ where $f$ is the frequency, $C_n$ is the nth element of the coefficients list, $e_n$ is the nth element of the exponents list, $f_n$ is the nth element of the frequencies list, and $H(x)$ are Heaviside distributions.

for scheme_objects in schemes.values():
    # Define amplitude noise spectral density according to a function
    # S(f) = 1.0e-3 * f^2, for 0.1 < f < 1.0
    nsd_amplitude = qctrl.factories.piecewise_power_law_noise_spectral_densities.create(
        noise=scheme_objects['amplitude_noise'],
        interpolated_frequencies=np.linspace(0.1, 1.0, 100),
        exponents=[2],
        coefficients=[1.0e-3],
        frequencies=[0.1, 1.0],
    )

    # Define dephasing noise spectral density according to a function
    # S(f) = 1.0e15 * f^1, for 0.005 < f < 1.0
    nsd_dephasing = qctrl.factories.piecewise_power_law_noise_spectral_densities.create(
        noise=scheme_objects['dephasing_noise'],
        interpolated_frequencies=np.linspace(0.005, 1.0, 100),
        exponents=[1],
        coefficients=[1.0e15],
        frequencies=[0.005, 1.0],
    )
    
    # Saving objects
    scheme_objects['nsd_amplitude'] = nsd_amplitude
    scheme_objects['nsd_dephasing'] = nsd_dephasing

Calculating the infidelity with respect to noise spectral densities

Once the spectral densities have been defined, we can calculate the infidelities with respect to their noise by running a calculation of the filter function. More details about how to perform this calculation can be found in the Filter functions feature guide.

The infidelity with respect to this spectral density is then stored in a variable called infidelity within the filter function object. A total infidelity taking into account all the noises is also available as the approximate_infidelity variable of the system.

for scheme, scheme_objects in schemes.items():
    # Calculate filter functions
    filter_function_amplitude = qctrl.services.filter_functions.calculate(
        scheme_objects['filter_function_amplitude'])
    filter_function_dephasing = qctrl.services.filter_functions.calculate(
        scheme_objects['filter_function_dephasing'])

    # Printing results
    print("Scheme: {}".format(scheme))
    print("Amplitude filter function infidelity: {}".format(filter_function_amplitude.infidelity))
    print("Dephasing filter function infidelity: {}".format(filter_function_dephasing.infidelity))

    scheme_objects['system'].refresh()
    print("Total filter function infidelity: {}".format(scheme_objects['system'].approximate_infidelity))
100%|██████████| 4/4 [00:44<00:00, 11.13s/it, running=0]
100%|██████████| 4/4 [00:35<00:00,  8.96s/it, running=0]
Scheme: primitive
Amplitude filter function infidelity: 0.00138595918058909
Dephasing filter function infidelity: 0.00196178460953228
Total filter function infidelity: 0.00334774379012137
100%|██████████| 4/4 [00:41<00:00, 10.34s/it, running=0]
100%|██████████| 4/4 [00:41<00:00, 10.37s/it, running=0]
Scheme: SK1
Amplitude filter function infidelity: 5.55111512312578e-17
Dephasing filter function infidelity: 0.00196177570519107
Total filter function infidelity: 0.00196177570519113
100%|██████████| 4/4 [00:37<00:00,  9.40s/it, running=0]
100%|██████████| 4/4 [00:38<00:00,  9.55s/it, running=0]
Scheme: CORPSE
Amplitude filter function infidelity: 0.00137311007945212
Dephasing filter function infidelity: 3.05018788004929e-11
Total filter function infidelity: 0.001373110109954

Computing noise-free infidelity

The Q-CTRL Python Package also allows us to compute the noise-free infidelity, which measures the performance of each system in the absence of any noise. To achieve this, we use the noise_free_metrics service for each system, as implemented below. We update the total infidelity by combining the noise-free infidelity with the filter function infidelity.

for scheme, scheme_objects in schemes.items():
    scheme_objects['system'].refresh()
    
    # Compute the noise-free infidelity
    target = qctrl.services.noise_free_metrics.calculate(scheme_objects['target'])
    print("System: " + scheme)
    print("Noise-free infidelity: " + str(target.infidelity))
    
    # Update the total infidelity
    scheme_objects['system'].refresh()
    print("Total infidelity: {}".format(scheme_objects['system'].approximate_infidelity))
100%|██████████| 4/4 [00:24<00:00,  6.04s/it, running=0]
System: primitive
Noise-free infidelity: 0.0
Total infidelity: 0.00334774379012137
100%|██████████| 4/4 [00:24<00:00,  6.13s/it, running=0]
System: SK1
Noise-free infidelity: 0.0
Total infidelity: 0.00196177570519113
100%|██████████| 4/4 [00:25<00:00,  6.48s/it, running=0]
System: CORPSE
Noise-free infidelity: 0.0
Total infidelity: 0.001373110109954

Summary

Noise-free infidelities for all the pulses are zero, as we only chose controls that are intended to achieve the operation that we wanted: in the absence of noise they work perfectly. For each pulse, we see that the total infidelities are dominated by the filter function infidelities. By their turn, the filter function infidelities are dominated by the infidelity of the noise against which the each pulse performs worse.

In the case of SK1, this means the dephasing noise, as this is a kind of control specifically designed to reduce amplitude noise. In the case of CORPSE the situation is inverted, as this pulse was designed to protect against dephasing noise rather than amplitude errors. The primitive is not particularly resistant to any kind of noise, in which case the two filter function infidelities are comparable, and just add up to the total process infidelity.

Example: performance of an optimized robust pulse for a single qubit undergoing amplitude and dephasing noise

The Q-CTRL Python Package is capable of generating optimized pulses for the system described above, if we follow the instruction from the Optimization feature guide. These pulses will be robust against both kinds of noise defined in the system, with the small trade-off of having a slightly imperfect gate when there is no noise. This illustrates a situation where all the infidelities calculated are small but non-zero, with the total infidelity still being smaller than all the pulses treated in the previous example.

# Define standard matrices
identity = np.array([[1., 0.], [0., 1.]], dtype=np.complex)
sigma_x = np.array([[0., 1.], [1., 0.]], 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)

# Defining target operation
total_rotation = np.pi/4.
azimuthal_angle = 0
target_unitary = np.cos(total_rotation/2.)*identity - 1.j*np.sin(total_rotation/2.)*sigma_x

# Define control parameters
omega_max = 2 * np.pi * 0.1e9  # Hz
delta_max = 2 * np.pi * 0.1e9  # Hz

# Define optimization parameters
segments = 10
duration = 2 * 1e-8  # s

# Define filter function parameters
sample_count = 3000
interpolated_amplitudes = np.logspace(-1, np.log10(omega_max), 1000)  # Hz
interpolated_detuning_frequencies = np.logspace(-2, np.log10(delta_max), 1000)  # Hz

# Define system object
system_2 = qctrl.factories.systems.create(
    name='optimal',
    hilbert_space_dimension=2,
)

# Define control objects
drive_2 = qctrl.factories.drive_controls.create(
    name='Microwave',
    operator=sigma_m/2.,
    system=system_2,
)

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

)

# Define optimal pulses
qctrl.factories.optimum_pulses.create(
    control=drive_2,
    segment_count=segments,
    upper_bound=omega_max,
    fixed_modulus=False,
    duration=duration,
)

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

# Define noises
amplitude_noise_2 = qctrl.factories.control_noises.create(
    name='Amplitude noise',
    control=drive_2
)

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

# Define target
target_2 = qctrl.factories.targets.create(
    unitary_operator=target_unitary,
    projection_operator=identity,
    system=system_2,
)

# Run optimization
system_2 = qctrl.services.robust_optimization.run(system_2)

# Define amplitude noise spectral density according to a function
# S(f) = 1.0e-3 * f^2, for 0.1 < f < 1.0
nsd_amplitude = qctrl.factories.piecewise_power_law_noise_spectral_densities.create(
    noise=amplitude_noise_2,
    interpolated_frequencies=np.linspace(0.1, 1.0, 100),
    exponents=[2],
    coefficients=[1.0e-3],
    frequencies=[0.1, 1.0],
)

# Define dephasing noise spectral density according to a function
# S(f) = 1.0e15 * f^1, for 0.005 < f < 1.0
nsd_dephasing = qctrl.factories.piecewise_power_law_noise_spectral_densities.create(
    noise=dephasing_noise_2,
    interpolated_frequencies=np.linspace(0.005, 1.0, 100),
    exponents=[1],
    coefficients=[1.0e15],
    frequencies=[0.005, 1.0],
)

# Define filter functions
filter_function_amplitude_2 = qctrl.factories.filter_functions.create(
    noise=amplitude_noise_2,
    sample_count=sample_count,
    interpolated_frequencies=interpolated_amplitudes,
)

filter_function_dephasing_2 = qctrl.factories.filter_functions.create(
    noise=dephasing_noise_2,
    sample_count=sample_count,
    interpolated_frequencies=interpolated_detuning_frequencies,
)

# Calculate filter functions
filter_function_amplitude_2 = qctrl.services.filter_functions.calculate(
    filter_function_amplitude_2)
filter_function_dephasing_2 = qctrl.services.filter_functions.calculate(
    filter_function_dephasing_2)

# Printing results
print("Amplitude filter function infidelity: {}".format(filter_function_amplitude_2.infidelity))
print("Dephasing filter function infidelity: {}".format(filter_function_dephasing_2.infidelity))

system_2.refresh()
print("Total filter function infidelity: {}".format(system_2.approximate_infidelity))

# Compute the noise-free infidelity
target_2 = qctrl.services.noise_free_metrics.calculate(target_2)
print("Noise-free infidelity: " + str(target_2.infidelity))

# Update the total infidelity
system_2.refresh()
print("Total infidelity: {}".format(system_2.approximate_infidelity))
100%|██████████| 23/23 [01:12<00:00,  3.13s/it, running=0]
100%|██████████| 4/4 [00:42<00:00, 10.73s/it, running=0]
100%|██████████| 4/4 [00:35<00:00,  8.78s/it, running=0]
Amplitude filter function infidelity: 1.01357283677217e-09
Dephasing filter function infidelity: 1.89301640474149e-08
Total filter function infidelity: 1.99437368841871e-08
100%|██████████| 4/4 [00:24<00:00,  6.04s/it, running=0]
Noise-free infidelity: 1.29971589046818e-11
Total infidelity: 1.99567340430918e-08

Wiki

Comprehensive knowledge base of quantum control theory

Explore