Filter functions

Calculation of noise filtering properties in driven controls

The Q-CTRL Python package allows the calculation of filter functions as a way of estimating the sensitivity of a control to the frequency of a time-dependent noise. In this guide we show how to define, calculate, and visualize filter functions obtained 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
from qctrlvisualizer import plot_filter_functions

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

Worked example: composite $\pi$ -pulses applied to a single qubit under amplitude and dephasing noise

In this example, we will compare the filter functions corresponding to different composite $\pi$-pulses under amplitude and dephasing noise. The Hamiltonian of the system we will be considering is:

$$ 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, $$

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 (with $\displaystyle \sigma_\pm = \frac{\sigma_x \pm i \sigma_y}{2}$).

We will consider the following driven control schemes for the controllable $\Omega (t)$ and $\Delta (t)$ terms:

  • primitive,
  • BB1,
  • CORPSE in BB1.

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

In this system, filter functions can be calculated for two kinds of noise, so that the sensitivity of the controls against them can be compared. We will compare the filter functions of each for a range of noise frequencies from $10^{-8} \Omega_\mathrm{max}$ to $\Omega_\mathrm{max}$, where $\Omega_\mathrm{max}/2\pi = 1 \mathrm{MHz}$ is the maximum Rabi frequency.

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', 'CORPSE', 'CORPSE in BB1']}

for scheme, scheme_objects in schemes.items():
    # Define system object
    system =

    # Define control objects
    drive = qctrl.factories.drive_controls.create(
        name='Rabi rate',
    shift = qctrl.factories.shift_controls.create(

    # Define pulse objects using pulses from Q-CTRL Open Controls
    pulse = new_predefined_driven_control(
    drive_pulse = qctrl.factories.custom_pulses.create(
        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(
        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 noise',

    dephasing_noise = qctrl.factories.additive_noises.create(

    # Save relevant objects for later use
    scheme_objects['system'] = system
    scheme_objects['amplitude_noise'] = amplitude_noise
    scheme_objects['dephasing_noise'] = dephasing_noise

Creating the filter functions

Filter functions are created using the qctrl.factories.filter_functions.create method. This method receives as parameters:

  • the noise for which the filter function will be calculated,
  • the sample_count number, which can be increased to improve the precision of the filter function values,
  • and an array of interpolated_frequencies, which correspond to the points where the filter function will be calculated.

In this example, the frequencies in the array will be spaced logarithmically, because we will also be interested in plotting the filter functions on a log-log graph.

# Define filter function parameters
sample_count = 3000
interpolated_frequencies = omega_max*np.logspace(-8, 0, 1000, base=10)

# Create filter function objects
for scheme_objects in schemes.values():
    scheme_objects['amplitude_filter_function'] = qctrl.factories.filter_functions.create(

    scheme_objects['dephasing_filter_function'] = qctrl.factories.filter_functions.create(

Calculating the filter functions

The method calculates the actual values of the filter function. This method takes the objects returned by qctrl.factories.filter_functions.create as input.

for scheme_objects in schemes.values():
    scheme_objects['calculated_amplitude_filter_function'] =
    scheme_objects['calculated_dephasing_filter_function'] =
100%|██████████| 4/4 [00:54<00:00, 13.68s/it, running=0]
100%|██████████| 4/4 [00:49<00:00, 12.41s/it, running=0]
100%|██████████| 4/4 [00:51<00:00, 12.90s/it, running=0]
100%|██████████| 4/4 [00:48<00:00, 12.11s/it, running=0]
100%|██████████| 4/4 [01:28<00:00, 22.25s/it, running=0]
100%|██████████| 4/4 [00:44<00:00, 11.19s/it, running=0]
100%|██████████| 4/4 [00:45<00:00, 11.42s/it, running=0]
100%|██████████| 4/4 [00:55<00:00, 13.75s/it, running=0]

Visualizing the filter functions

After their calculation, the numerical values of the filter functions are stored at filter_function.interpolated_points (where filter_function is the object returned by the calculate method). Each point contains a corresponding frequency (our x coordinates), an inverse_power (our y coordinates), and an inverse_power_precision (our error bars).

All this information is handled automatically when plotted using the plot_filter_functions method from the Q-CTRL Python Visualizer package.

Amplitude noise

                      {scheme: scheme_objects['calculated_amplitude_filter_function'].interpolated_points
                       for scheme, scheme_objects in schemes.items()})

Dephasing noise

                      {scheme: scheme_objects['calculated_dephasing_filter_function'].interpolated_points
                       for scheme, scheme_objects in schemes.items()})


The plots show that the BB1 controls perform better against low-frequency amplitude noise than the primitive $\pi$-pulse, but perform just like the primitive in the case of dephasing noise. This is expected, as BB1 is one of the control-error-compensating driven controls. The inverse is true of the pure CORPSE controls: they perform as poorly as the primitive against amplitude noise, but perform better against low-frequency dephasing noise. This behavior is expected as CORPSE is a dephasing-error-compensating driven control. Finally, the dephasing-and-control-error-compensating driven control CORPSE in BB1 performs better than the primitive for low-frequencies of both noises (albeit less so than the controls specialized for one specific kind of noise).

We have thus demonstrated how the Q-CTRL Python package can be used to characterize the sensitivity of different controls to time-dependent noise channels by calculating their corresponding filter functions.


Comprehensive knowledge base of quantum control theory