diff --git a/qualang_tools/octave_tools/calibration_result_plotter.py b/qualang_tools/octave_tools/calibration_result_plotter.py index e6a890b6..d22c70ce 100644 --- a/qualang_tools/octave_tools/calibration_result_plotter.py +++ b/qualang_tools/octave_tools/calibration_result_plotter.py @@ -36,11 +36,6 @@ class CalibrationResultPlotter: def __init__(self, data: MixerCalibrationResults): self.data = data - output_gain = list(self.data.keys())[0][1] - self.lo_frequency = list(self.data.keys())[0][0] - self.if_data = data[(self.lo_frequency, output_gain)].image - self.lo_data = data[(self.lo_frequency, output_gain)] - self.if_frequency = list(self.if_data.keys())[0] @staticmethod def _handle_zero_indices_and_masking(data): @@ -76,22 +71,48 @@ def _convert_to_dbm(volts): def show_lo_result( self, + lo_freq: Optional[Number] = None, + label: str = "", ) -> None: """ Show the result of LO leakage automatic calibration. + + Args: + lo_freq (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. + label (str): A label to be used in the plot title. """ + if lo_freq is None: + if len(self.data) == 1: + lo_freq = list(self.data.keys())[0][0] + + else: + print(f"There is a calibration data for {len(self.data)} LO frequencies. You must choose one") + return + + try: + output_gain = list(self.data.keys())[0][1] + except IndexError: + print("No valid calibration result") + return + + if (lo_freq, output_gain) not in self.data: + print(f"No calibration data for LO frequency = {lo_freq / 1e9:.3f}GHz") + return plt.figure(figsize=(11, 8.9)) + # plt.title(f"LO auto calibration @ {lo_freq/1e9:.3f}GHz") + + lo_data = self.data[(lo_freq, output_gain)] plt.subplot(222) - d = self.lo_data.debug.coarse[0] + d = lo_data.debug.coarse[0] - q_scan = d.q_scan * 1000 # convert to mV + q_scan = d.q_scan * 1000 i_scan = d.i_scan * 1000 zero_list = self._handle_zero_indices_and_masking(d.lo) - lo = self.u.demod2volts(d.lo, 10_000) + lo = self.u.demod2volts(d.lo, integration_length) lo_dbm = self._convert_to_dbm(lo) dq = np.mean(np.diff(q_scan, axis=1)) di = np.mean(np.diff(i_scan, axis=0)) @@ -104,7 +125,7 @@ def show_lo_result( plt.text( np.min(q_scan) + 0.5 * dq, np.max(i_scan) - 0.5 * di, - f"coarse scan\nLO = {self.lo_frequency / 1e9:.3f}GHz", + f"{label}coarse scan\nLO = {lo_freq / 1e9:.3f}GHz", color="k", bbox=dict(facecolor="w", alpha=0.8), verticalalignment="top", @@ -166,11 +187,13 @@ def show_lo_result( verticalalignment="top", ) - fine_debug_data = self.lo_data.debug.fine + fine_debug_data = lo_data.debug.fine + if fine_debug_data is None: + raise ValueError("No fine debug data, cannot plot.") x0_ref, y0_ref = x0, y0 - d = fine_debug_data[0] + d = lo_data.debug.fine[0] x0_fine = d.fit.x_min * 1000 + x0_ref y0_fine = d.fit.y_min * 1000 + y0_ref @@ -195,7 +218,7 @@ def show_lo_result( zero_list = self._handle_zero_indices_and_masking(d.lo) - lo = self.u.demod2volts(d.lo, 10_000) + lo = self.u.demod2volts(d.lo, integration_length) lo_dbm = self._convert_to_dbm(lo) width = q_scan[0][1] - q_scan[0][0] @@ -251,7 +274,7 @@ def show_lo_result( t = plt.text( np.min(fine_q_scan) + 0.5 * dq, np.max(fine_i_scan) - 0.5 * di, - f"fine scan\nLO = {self.lo_frequency / 1e9:.3f}GHz", + f"fine scan\nLO = {lo_freq / 1e9:.3f}GHz", color="k", verticalalignment="top", ) @@ -272,7 +295,7 @@ def show_lo_result( p = d.fit.pol iq_error = self.u.demod2volts( - p[0] + p[1] * X + p[2] * Y + p[3] * X**2 + p[4] * X * Y + p[5] * Y**2 - d.lo, 10_000 + p[0] + p[1] * X + p[2] * Y + p[3] * X**2 + p[4] * X * Y + p[5] * Y**2 - d.lo, integration_length ) iq_error_dbm = self._convert_to_dbm(np.abs(iq_error)) @@ -305,7 +328,7 @@ def show_lo_result( "Current result:", f" I_dc = {y0:.02f}mV, Q_dc = {x0:.2f}mV", "\nAchieved LO supression:", - f"{self._get_lo_suppression():.3f} dB", + f"{self.get_lo_suppression(data):.3f} dB", ] plt.text( @@ -319,22 +342,58 @@ def show_lo_result( plt.box(False) plt.xticks([]) plt.yticks([]) - plt.suptitle(f"LO auto calibration @ {self.lo_frequency/1e9:.3f}GHz") + plt.suptitle(f"LO auto calibration @ {lo_freq/1e9:.3f}GHz") plt.tight_layout() def show_if_result( self, + lo_freq: Optional[float] = None, + if_freq: Optional[float] = None, + label: str = "", ) -> None: """ Show the result of image sideband automatic calibration. Args: - lo_frequency (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. + lo_freq (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. if_freq (Optional[float]): The IF frequency in Hz. If not provided, the first IF frequency in data is used. label (str): A label to be used in the plot title. """ + if lo_freq is None: + if len(self.data) == 1: + lo_freq = list(self.data)[0][0] + + else: + print(f"There is a calibration data for {len(self.data)} LO frequencies. You must choose one") + return + + try: + output_gain = list(self.data)[0][1] + except IndexError: + print("No valid calibration result") + return + + if (lo_freq, output_gain) not in self.data: + print(f"No calibration data for LO frequency = {lo_freq/1e9:.3f}GHz") + return + + if_data = self.data[(lo_freq, output_gain)].image - if_freq_data = self.if_data[self.if_frequency] + if len(if_data) == 0: + print(f"No IF calibration results for LO frequency = {lo_freq/1e9:.3f}GHz") + return + + if if_freq is None: + if len(if_data) == 1: + if_freq = list(if_data.keys())[0] + else: + logger.debug( + f"There is a calibration data for {len(if_data)} IF frequencies for " + f"LO frequency = {lo_freq/1e9:.3f}GHz. You must choose one" + ) + return + + if_freq_data = if_data[if_freq] plt.figure(figsize=(11, 8.9)) @@ -346,7 +405,7 @@ def show_if_result( zero_list = self._handle_zero_indices_and_masking(r.image) - im = self.u.demod2volts(r.image, 10_000) + im = self.u.demod2volts(r.image, integration_length) im_dbm = self._convert_to_dbm(im) width = r.p_scan[0][1] - r.p_scan[0][0] @@ -369,7 +428,7 @@ def show_if_result( plt.text( np.min(r.p_scan) + 1.5 * dp, np.max(r.g_scan - 1.5 * dg), - f"coarse scan\nLO = {self.lo_frequency/1e9:.3f}GHz\nIF = {self.if_frequency/1e6:.3f}MHz", + f"{label}coarse scan\nLO = {lo_freq/1e9:.3f}GHz\nIF = {if_freq/1e6:.3f}MHz", color="k", bbox=dict(facecolor="w", alpha=0.8), verticalalignment="top", @@ -400,7 +459,7 @@ def show_if_result( zero_list = self._handle_zero_indices_and_masking(r.image) - im = self.u.demod2volts(r.image, 10_000) + im = self.u.demod2volts(r.image, integration_length) im_dbm = self._convert_to_dbm(im) width = r.p_scan[0][1] - r.p_scan[0][0] @@ -424,7 +483,7 @@ def show_if_result( plt.text( np.min(r.p_scan) + 1.5 * dp, np.max(r.g_scan - 1.5 * dg), - f"fine scan\nLO = {self.lo_frequency/1e9:.3f}GHz\nIF = {self.if_frequency/1e6:.3f}MHz", + f"{label}fine scan\nLO = {lo_freq/1e9:.3f}GHz\nIF = {if_freq/1e6:.3f}MHz", color="k", bbox=dict(facecolor="w", alpha=0.8), verticalalignment="top", @@ -436,21 +495,24 @@ def show_if_result( p = r.fit.pol image = self.u.demod2volts( - p[0] + p[1] * X + p[2] * Y + p[3] * X**2 + p[4] * X * Y + p[5] * Y**2 - r.image, 10_000 + p[0] + p[1] * X + p[2] * Y + p[3] * X**2 + p[4] * X * Y + p[5] * Y**2 - r.image, integration_length ) image_dbm = self._convert_to_dbm(np.abs(image)) - self._plot_scan(r.p_scan, r.g_scan, image_dbm, zero_list, width, height, "phase (rad)", "gain") + plt.pcolor(r.p_scan, r.g_scan, image_dbm, cmap=CalibrationResultPlotter.custom_cmap) + plt.xlabel("phase (rad)") + plt.ylabel("gain") + plt.axis("equal") plt.plot(r.phase, r.gain, "yo", markersize=8) plt.plot(r.phase, r.gain, "ro", markersize=4) - # plt.colorbar(label="Power [dBm]") + plt.colorbar(label="Power [dBm]") plt.text( np.min(r.p_scan) + 1.5 * dp, np.max(r.g_scan - 1.5 * dg), - f"fit error\nLO = {self.lo_frequency/1e9:.3f}GHz\nIF = {self.if_frequency/1e6:.3f}MHz", + f"{label}fit error\nLO = {lo_freq/1e9:.3f}GHz\nIF = {if_freq/1e6:.3f}MHz", color="k", bbox=dict(facecolor="w", alpha=0.8), verticalalignment="top", @@ -465,7 +527,7 @@ def show_if_result( "Calibrated parameters:", f" gain = {r.gain*100:.02f}%, phase = {r.phase*180.0/np.pi:.2f}deg", "\nAchieved Image sideband supression:", - f"{self._get_if_suppression():.3f} dB", + f"{self.get_if_suppression(data):.3f} dB", ] plt.text( @@ -479,14 +541,12 @@ def show_if_result( plt.box(False) plt.xticks([]) plt.yticks([]) - plt.suptitle( - f"IMAGE auto calibration: LO = {self.lo_frequency/1e9:.3f}GHz, IF = {self.if_frequency/1e6:.3f}MHz" - ) + plt.suptitle(f"IMAGE auto calibration: LO = {lo_freq/1e9:.3f}GHz, IF = {if_freq/1e6:.3f}MHz") plt.tight_layout() def _get_if_suppression( self, - lo_frequency: Optional[float] = None, + lo_freq: Optional[float] = None, if_freq: Optional[float] = None, ): """ @@ -496,14 +556,47 @@ def _get_if_suppression( If the IF frequency is not given, the first IF frequency in the data is used. Args: - lo_frequency (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. + lo_freq (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. if_freq (Optional[float]): The IF frequency in Hz. If not provided, the first IF frequency in data is used. Returns: float: The reduction of Image sideband power before vs after calibration in dB units. """ + if lo_freq is None: + if len(self.data) == 1: + lo_freq = list(self.data)[0][0] + + else: + print(f"There is a calibration data for {len(self.data)} LO frequencies. You must choose one") + return + + try: + output_gain = list(self.data)[0][1] + except IndexError: + print("No valid calibration result") + return - if_freq_data = self.if_data[self.if_frequency] + if (lo_freq, output_gain) not in self.data: + print(f"No calibration data for LO frequency = {lo_freq/1e9:.3f}GHz") + return + + if_data = self.data[(lo_freq, output_gain)].image + + if len(if_data) == 0: + print(f"No IF calibration results for LO frequency = {lo_freq/1e9:.3f}GHz") + return + + if if_freq is None: + if len(if_data) == 1: + if_freq = list(if_data.keys())[0] + else: + logger.debug( + f"There is a calibration data for {len(if_data)} IF frequencies for " + f"LO frequency = {lo_freq/1e9:.3f}GHz. You must choose one" + ) + return + + if_freq_data = if_data[if_freq] image_fine = if_freq_data.fine.image if image_fine.min() > 0.0: @@ -512,18 +605,19 @@ def _get_if_suppression( mask = image_fine == 0.0 image_fine[mask] = np.nan - image = self.u.demod2volts(image_fine, 10_000) + image = self.u.demod2volts(image_fine, integration_length) image_array_dbm = self._convert_to_dbm(image) min_image_dbm = np.nanmin(image_array_dbm) pol = if_freq_data.fine.fit.pol image_0 = self._paraboloid(0, 0, pol) - image_0_volts = self.u.demod2volts(image_0, 10_000) + image_0_volts = self.u.demod2volts(image_0, integration_length) image_0_dbm = self._convert_to_dbm(image_0_volts) return min_image_dbm - image_0_dbm def _get_lo_suppression( self, + lo_freq: Optional[float] = None, ): """ Calculate the LO leakage suppression achieved by the automatic calibration. @@ -531,15 +625,37 @@ def _get_lo_suppression( If the LO frequency is not given, the first LO frequency in the data is used. If the IF frequency is not given, the first IF frequency in the data is used. + Args: + lo_freq (Optional[float]): The LO frequency in Hz. If not provided, the first LO frequency in data is used. + Returns: float: The reduction of LO leakage power before vs after calibration in dB units. """ + if lo_freq is None: + if len(self.data) == 1: + lo_freq = list(self.data.keys())[0][0] + + else: + print(f"There is a calibration data for {len(self.data)} LO frequencies. You must choose one") + return + + try: + output_gain = list(self.data.keys())[0][1] + except IndexError: + print("No valid calibration result") + return + + if (lo_freq, output_gain) not in self.data: + print(f"No calibration data for LO frequency = {lo_freq / 1e9:.3f}GHz") + return + + lo_data = self.data[(lo_freq, output_gain)] - i_coarse = self.lo_data.debug.coarse[0].i_scan - q_coarse = self.lo_data.debug.coarse[0].q_scan + i_coarse = lo_data.debug.coarse[0].i_scan + q_coarse = lo_data.debug.coarse[0].q_scan - lo_coarse = self.lo_data.debug.coarse[0].lo - lo_fine = self.lo_data.debug.fine[0].lo + lo_coarse = lo_data.debug.coarse[0].lo + lo_fine = lo_data.debug.fine[0].lo if lo_fine.min() > 0.0: pass @@ -547,14 +663,14 @@ def _get_lo_suppression( mask = lo_fine == 0.0 lo_fine[mask] = np.nan - lo = self.u.demod2volts(lo_fine, 10_000) + lo = self.u.demod2volts(lo_fine, integration_length) lo_array_dbm = self._convert_to_dbm(lo) min_lo_dbm = np.nanmin(lo_array_dbm) id_i = np.argwhere(i_coarse[:, 0] == 0.0) id_q = np.argwhere(q_coarse[0, :] == 0.0) lo = lo_coarse[id_i, id_q][0][0] - lo_0 = self.u.demod2volts(lo, 10_000) + lo_0 = self.u.demod2volts(lo, integration_length) lo_0_dbm = self._convert_to_dbm(lo_0) return min_lo_dbm - lo_0_dbm