# 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.

```
# 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,
- 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.

```
# 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 = 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 noise',
control=drive,
)
dephasing_noise = qctrl.factories.additive_noises.create(
name='Dephasing',
system=system,
operator=sigma_z/2.,
)
# 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(
noise=scheme_objects['amplitude_noise'],
sample_count=sample_count,
interpolated_frequencies=interpolated_frequencies,
)
scheme_objects['dephasing_filter_function'] = qctrl.factories.filter_functions.create(
noise=scheme_objects['dephasing_noise'],
sample_count=sample_count,
interpolated_frequencies=interpolated_frequencies,
)
```

```
for scheme_objects in schemes.values():
scheme_objects['calculated_amplitude_filter_function'] = qctrl.services.filter_functions.calculate(
scheme_objects['amplitude_filter_function']
)
scheme_objects['calculated_dephasing_filter_function'] = qctrl.services.filter_functions.calculate(
scheme_objects['dephasing_filter_function']
)
```

### 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.

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

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

### Summary

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.