
# Digital filter correction fitting functions

## Overview of digital filters

When operating qubits and aiming for high-fidelity single- and two-qubit gates, performance is often limited by the quality of the control pulses generated by the room-temperature controller. Distortions are introduced by the analog setup that connects the controller to the qubits. For example, due to impedance mismatches in the transmission lines. Two main types of filters are typically required:

 - Short timescale distortions (< 50 ns): These appear as fast, unwanted variations in the control signal. Such deviations can be corrected using FIR (finite impulse response) filters.
 - Long timescale distortions (> 50 ns): These manifest as additional RC-like filtering of the signal, leading to long saturation times. Such distortions can be corrected using IIR (infinite impulse response) filters.

This file provides tools for calculating the correction parameters needed to implement digital filters with the OPX1000. In particular, it focuses on generating the IIR filter coefficients that can be directly inserted into the OPX1000 configuration file, making it seamless to implement high-fidelity gates.

More background on digital filters can be found in the [QUA documentation page](https://docs.quantum-machines.co/latest/docs/Guides/output_filter).

## IIR correction filter functions

This repository is optimized for estimating IIR filter corrections and includes tools to fit time-series data with multiple exponential decay components. It is especially useful for analyzing physical systems (e.g., flux or exchange pulse response signals) where the response can be modeled as a sum of exponential decays plus a constant background term.

The workflow assumes that distorted pulse data have already been measured at the qubit level and saved in a ```.h5``` (NetCDF) file.

### Features
1. Single exponential model implements a simple exponential decay model: $f(t) = Ae^{(-t/\tau)}$
2. Sequential multi-exponential fitting
    - Automatically estimates a constant (DC) offset from the data tail
    - Fits the slowest time constants first, subtracts them, and then extracts faster components.
    - Can constrain decay constants $\tau$ or fit them freely.

### Workflow
1. Import data in ```.h5``` (NetCDF) file.
2. Define the ```fitting_start_fractions```. This list defines the number of exponential components (by its size) and provides heuristic, user-chosen starting guesses for their regions.
These fractions are optimized automatically, but the number of exponents is fixed; in the future, this selection will be automated to avoid user intervention.
3. Run the optimization ```optimize_start_fractions``` and plot the final result using ```plot_fit```
4. Check your ```residuals``` to make sure that the fit is reliable
5. The tuple list of $(A, \tau)$ can now be copied and pasted directly in the configuration file in the ```filter``` -> ```exponential``` section.
6. Watch the magic unfold and boost your gate fidelity

### Example

Below is an example of implementing the IIR digital filter using the OPX1000 to correct distortions in a flux-tunable superconducting qubit.

The original uncorrected control pulse was obtained through the [cryoscope experiment](https://pubs.aip.org/aip/apl/article/116/5/054001/38884/Time-domain-characterization-and-correction-of-on), where the LF-FEM channel of the OPX1000 was used to control the qubit flux response. Here the corresponding [QUA implementation](https://github.com/qua-platform/qua-libs/blob/main/Quantum-Control-Applications/Superconducting/Single-Flux-Tunable-Transmon/17_cryoscope_1ns.py). Because the flux pulse is distorted by the dilution refrigerator transmission lines, the qubit exhibits a quantum state response directly dependent on the pulse distortion. From this, the actual pulse shape at the qubit can be reconstructed.

As the data are saved in the ```.h5``` file, we can now follow the workflow introduced above:
````python
from qualang_tools.digital_filters import optimize_start_fractions, plot_fit
import numpy as np
import netCDF4 as nc

# load data
with nc.Dataset('path_to_dataset.h5', 'r') as f:
    t_data = np.array(f.variables['time'][:])
    y_data = np.squeeze(np.array(f.variables['flux_response'][:]))

# fit data
# fitting_start_fractions = [0.6, 0.3, 0.02]
fitting_start_fractions = [0.8, 0.6, 0.3, 0.2, 0.02]
success, best_fractions, components, a_dc, best_rms = optimize_start_fractions(t_data, y_data, fitting_start_fractions)

# plot data and fit
fig, axs = plot_fit(t_data, y_data, components, a_dc)

# parameters to update in config
A_list = [component[0] / a_dc for component in components]
tau_list = [component[1] for component in components]
exponential_filter = list(zip(A_list, tau_list))
# Assuming that we want to update the filters on analog output port 1 of LF-fem 1
output_port = config["controllers"]["con1"]["fems"][1]["analog_outputs"][1]
output_port["filter"] = {
   "exponential_dc_gain": a_dc,
   "exponential": exponential_filter,
   "feedforward": []
}
````
The exact implementation depends on the data and the number of exponential components to correct.
For instance, on a given qubit and using ```fitting_start_fractions``` = [0.6, 0.3, 0.02] we can obtain the following result:

<img src="three_exponentials.png" alt="drawing" width="950"/>

In this case, only three coefficients were required to reproduce the transfer function of the setup, and these coefficients can be directly used in the configuration file.

Here is a second example, measured on the same setup but from a different qubit. 
This time we used: ```fitting_start_fractions``` = [0.8, 0.6, 0.3, 0.2, 0.02] and obtained the following result:

<img src="five_exponentials.png" alt="drawing" width="950"/>

In this second case, five coefficients were required to fully reproduce the transfer function because the high-frequency coaxial cable in the fridge was different.