Source code for qblox_scheduler.analysis.calibration

# Repository: https://gitlab.com/qblox/packages/software/qblox-scheduler
# Licensed according to the LICENSE file on the main branch
#
# Copyright 2020-2025, Quantify Consortium
# Copyright 2025, Qblox B.V.
"""
Module containing analysis utilities for calibration procedures.

In particular, manipulation of data and calibration points for qubit readout
calibration.
"""

from __future__ import annotations

import numpy as np


[docs] def rotate_to_calibrated_axis( data: np.ndarray, ref_val_0: complex, ref_val_1: complex ) -> np.ndarray: """ Rotates, normalizes and offsets complex valued data based on calibration points. Parameters ---------- data An array of complex valued data points. ref_val_0 The reference value corresponding to the 0 state. ref_val_1 The reference value corresponding to the 1 state. Returns ------- : Calibrated array of complex data points. """ rotation_anle = np.angle(ref_val_1 - ref_val_0) norm = np.abs(ref_val_1 - ref_val_0) offset = ref_val_0 * np.exp(-1j * rotation_anle) / norm corrected_data = data * np.exp(-1j * rotation_anle) / norm - offset return corrected_data
[docs] def has_calibration_points( s21: np.ndarray, indices_state_0: tuple = (-2,), indices_state_1: tuple = (-1,) ) -> bool: r""" Determine if dataset with S21 data has calibration points for 0 and 1 states. Three pieces of information are used to infer the presence of calibration points: - The angle of the calibration points with respect to the average of the datapoints, - The distance between the calibration points, and - The average distance to the line defined be the calibration points. The detection is made robust by averaging 3 datapoints for each extremity of the "segment" described by the data on the IQ-plane. .. seealso:: :ref:`howto-analysis-has-calibration-points` Parameters ---------- s21 Array of complex datapoints corresponding to the experiment on the IQ plane. indices_state_0 Indices in the ``s21`` array that correspond to the ground state. indices_state_1 Indices in the ``s21`` array that correspond to the first excited state. Returns ------- : The inferred presence of calibration points. """ indices_state_0 = np.asarray(indices_state_0) indices_state_1 = np.asarray(indices_state_1) def _arg_min_n(array: np.ndarray, num: int): return np.argpartition(array, num)[:num] def _arg_max_n(array: np.ndarray, num: int): return np.argpartition(array, -num)[-num:] not_cal: np.ndarray = np.ones(s21.shape, dtype=bool) not_cal[indices_state_0] = False not_cal[indices_state_1] = False # do not include the potential calibration points since that can significantly # affect if the most of the data is far away from one of the calibration points magnitude_no_cal: np.ndarray = np.abs(s21[not_cal]) # Use the 3 points with maximum magnitude for resilience against noise and # outliers arg_max_no_cal: list = list(_arg_max_n(magnitude_no_cal, 3)) # Move one side of the "segment" described by the data on the IQ-plane to the # center of the IQ plane. This is necessary for the arg_max and arg_min of the # magnitude to correspond to the "segment" extremities. s21_shifted: np.ndarray = s21 - s21[arg_max_no_cal].mean() maybe_cal_pnts_0: np.ndarray = s21_shifted[indices_state_0].mean() maybe_cal_pnts_1: np.ndarray = s21_shifted[indices_state_1].mean() magnitude: float = np.abs(s21_shifted) arg_max: list = list(_arg_max_n(magnitude, 3)) arg_min: list = list(_arg_min_n(magnitude, 3)) center: complex = s21_shifted[arg_min + arg_max].mean() # center of the "segment" maybe_cal_pnts: np.ndarray = np.array((maybe_cal_pnts_0, maybe_cal_pnts_1)) angles: np.ndarray = np.angle(maybe_cal_pnts - center, deg=True) angles_diff: float = angles.max() - angles.min() avg_max: complex = s21_shifted[arg_max].mean() avg_min: complex = s21_shifted[arg_min].mean() segment_len: float = np.abs(avg_max - avg_min) cal_dist: float = np.abs(maybe_cal_pnts_0 - maybe_cal_pnts_1) far_enough: bool = cal_dist > 0.5 * segment_len def _cross_prod_on_plane(num_a: complex, num_b: complex): return num_a.real * num_b.imag - num_b.real * num_a.imag def _dist_to_line(point_a: complex, point_b: complex, point_c: complex): vec_a = point_b - point_a vec_b = point_c - point_a return np.abs(_cross_prod_on_plane(vec_a, vec_b)) / np.abs(vec_a) # to exclude some false positives confirm that most of the data is within a circle # with radius equal to half the distance between the calibration points dist_to_line: np.ndarray = _dist_to_line(maybe_cal_pnts_0, maybe_cal_pnts_1, s21_shifted) data_close_enough_to_line: bool = dist_to_line.mean() < cal_dist / 4 good_angle: bool = angles_diff > 90 has_cal_pnts: bool = far_enough and good_angle and data_close_enough_to_line return has_cal_pnts