Source code for ooragan.plotting

from numpy.typing import ArrayLike, NDArray
from typing import Optional, Literal, Iterable
from resonator import base
from graphinglib import SmartFigure, Curve, Inherit, INHERIT
import graphinglib as gl
import numpy as np
from scipy.constants import e, hbar, k

from .util import convert_complex_to_magphase
from .typing import _FitResult


FREQ_UNIT_CONVERSION = {"GHz": 1e9, "MHz": 1e6, "kHz": 1e3}


[docs] def plot_triptych( freq: ArrayLike, complex_data: ArrayLike, resonator_fitter: Optional[base.ResonatorFitter] = None, freq_unit: Literal["GHz", "MHz", "kHz"] = "GHz", title: Optional[str] = None, three_ticks: bool = False, figure_style: str | Inherit = INHERIT, ) -> SmartFigure: """ Plots the magnitude vs frequency, the phase vs frequency and the complex data in a single figure. Parameters ---------- freq : ArrayLike Frequency array. complex : ArrayLike Complex data array. resonator_fitter : ResonatorFitter, optional Fit result from the resonator library. Defaults to ``None``. .. note:: If a ``ResonatorFitter`` is provided, the fit and the resonance frequency obtained from the fit are displayed. freq_unit : {"GHz", "MHz", "kHz"}, optional Unit in which the frequency is given. Defaults to ``"GHz"``. title : str, optional Title of the figure. Defaults to ``None``. three_ticks : bool, optional If ``True``, only three ticks will be displayed on the x axis: the minimum frequency, the maximum and the frequency. Defaults to ``False``. figure_style : str or Inherit, optional GraphingLib figure style to apply to the plot. See `here <https://www.graphinglib.org/doc-1.5.0/handbook/figure_style_file.html#graphinglib-styles-showcase>`_ for more info. Examples -------- .. plot:: import numpy as np from ooragan import plot_triptych # Generate data from theoretical model def S21(f, Q, Qc, f0): return 1 - (Q / Qc) / (1 + 2j * Q * (f - f0) / f0) Q = 85000 Qc = 100000 f0 = 6.5e9 tau = 0.0000001 a = 1 alpha = 0 phi = 0 frequency = np.linspace(6.4995e9, 6.5005e9, 5001) fig = plot_triptych( frequency, S21(frequency, Q, Qc, f0), ) fig.show() """ freq = np.asarray(freq) / FREQ_UNIT_CONVERSION[freq_unit] real = np.real(complex_data) imag = np.imag(complex_data) mag, phase = convert_complex_to_magphase(real, imag) mag_vs_freq = gl.Scatter(freq, mag, marker_style=".") phase_vs_freq = gl.Scatter(freq, phase, marker_style=".") col1 = gl.SmartFigure( 2, 1, sub_x_labels=[None, "Fréquence (GHz)"], sub_y_labels=["$|S_{21}|$ (dB)", "arg($S_{21}$) (deg)"], share_x=True, reference_labels=False, height_ratios=[1, 1.3], elements=[mag_vs_freq, phase_vs_freq], figure_style=figure_style, ) complex = gl.Scatter(real, imag, marker_style=".") hline = gl.Hlines([0], line_styles=":", line_widths=1, colors="silver") vline = gl.Vlines([0], line_styles=":", line_widths=1, colors="silver") fig_complex = gl.SmartFigure( x_label=r"$\mathrm{Re}(S_{21})$", y_label=r"$\mathrm{Im}(S_{21})$", figure_style=figure_style, aspect_ratio="equal", reference_labels=False, elements=[complex, hline, vline], ) triptych = gl.SmartFigure( 1, 2, size=(10, 6), reference_labels=False, figure_style=figure_style, elements=[col1, fig_complex], title=title, ) if resonator_fitter is not None: fit = resonator_fitter.evaluate_fit(resonator_fitter.frequency) fr = resonator_fitter.evaluate_fit(resonator_fitter.resonance_frequency) mag_fit = gl.Curve( resonator_fitter.frequency / FREQ_UNIT_CONVERSION[freq_unit], 20 * np.log10(np.abs(fit)), color="k", line_width=1, ) mag_point = gl.Scatter( resonator_fitter.resonance_frequency / FREQ_UNIT_CONVERSION[freq_unit], 20 * np.log10(np.abs(fr)), face_color="k", ) phase_fit = gl.Curve( resonator_fitter.frequency / FREQ_UNIT_CONVERSION[freq_unit], np.degrees(np.angle(fit)), color="k", line_width=1, ) phase_point = gl.Scatter( resonator_fitter.resonance_frequency / FREQ_UNIT_CONVERSION[freq_unit], np.degrees(np.angle(fr)), face_color="k", ) complex_fit = gl.Curve( fit.real, fit.imag, color="k", line_width=1, label="Best fit" ) complex_point = gl.Scatter(fr.real, fr.imag, face_color="k", label="Resonance") triptych[0, 0][0, 0] += [mag_fit, mag_point] triptych[0, 0][1, 0] += [phase_fit, phase_point] triptych[0, 1].add_elements(complex_fit, complex_point) if three_ticks: triptych[0, 0].set_ticks(x_ticks=[np.min(freq), np.mean(freq), np.max(freq)]) triptych[0, 1].set_ticks( x_ticks=[np.min(real), 0, np.max(real)], y_ticks=[np.min(imag), 0, np.max(imag)], ) return triptych
def _as_function_of_photon_nbr( elements: Iterable[Curve], y_label: str, figure_style: str | Inherit, title: Optional[str], ) -> SmartFigure: fig = SmartFigure( x_label=r"$\tilde n$", y_label=y_label, elements=elements, figure_style=figure_style, title=title, log_scale_x=True, log_scale_y=True, legend_loc="outside center right", ) return fig
[docs] def plot_quality_factors( fit_results: _FitResult | list[_FitResult], show_Qc: bool = True, freq_unit: Literal["GHz", "MHz", "kHz"] = "GHz", title: Optional[str] = None, figure_style: str | Inherit = INHERIT, ) -> SmartFigure: r""" Plots the quality factors :math:`Q_i` and :math:`Q_c` as a function of average photon number in the resonator :math:`(\tilde n)`. Parameters ---------- fit_results : FitResult or list of FitResult Single or list of FitResult from a Fitter. show_Qc : bool, optional Whether or not to show both the :math:`Q_i` and the :math:`Q_c`. Defaults to ``True``. freq_unit : {"GHz", "MHz", "kHz"}, optional Unit in which the frequency is given. Defaults to ``"GHz"``. title : str, optional Title of the figure. Defaults to ``None``. figure_style : str or Inherit, optional GraphingLib figure style to apply to the plot. See `here <https://www.graphinglib.org/doc-1.5.0/handbook/figure_style_file.html#graphinglib-styles-showcase>`_ for more info. """ if isinstance(fit_results, _FitResult): fit_results = [fit_results] elif isinstance(fit_results, list): if not all(isinstance(i, _FitResult) for i in fit_results): raise TypeError("can only accept a FitResult or a list of FitResults") else: raise TypeError("can only accept a FitResult or a list of FitResults") elements = [] if not isinstance(figure_style, str): figure_style = gl.get_default_style() for i, fr in enumerate(fit_results): qi = Curve( fr.photon_nbr, fr.Q_i, label="{:.3f} {}".format( np.mean(fr.f_r) / FREQ_UNIT_CONVERSION[freq_unit], freq_unit ), color=gl.get_color(figure_style, i), ) elements.append(qi) if show_Qc: qc = Curve( fr.photon_nbr, fr.Q_c, line_style="--", color=gl.get_color(figure_style, i), ) elements.append(qc) y_label = "$Q_i, Q_c$" if show_Qc else "$Q_i$" fig = _as_function_of_photon_nbr( elements=elements, y_label=y_label, figure_style=figure_style, title=title ) return fig
[docs] def plot_losses( fit_results: _FitResult | list[_FitResult], show_deltac: bool = True, freq_unit: Literal["GHz", "MHz", "kHz"] = "GHz", title: Optional[str] = None, figure_style: str | Inherit = INHERIT, ) -> SmartFigure: r""" Plots the losses :math:`\delta_i` and :math:`\delta_c` as a function of average photon number in the resonator :math:`(\tilde n)`. Parameters ---------- fit_results : FitResult or list of FitResult Single or list of FitResult from a Fitter. show_deltac : bool, optional Whether or not to show both the :math:`\delta_i` and the :math:`\delta_c`. Defaults to ``True``. freq_unit : {"GHz", "MHz", "kHz"}, optional Unit in which the frequency is given. Defaults to ``"GHz"``. title : str, optional Title of the figure. Defaults to ``None``. figure_style : str, optional GraphingLib figure style to apply to the plot. See `here <https://www.graphinglib.org/doc-1.5.0/handbook/figure_style_file.html#graphinglib-styles-showcase>`_ for more info. """ if isinstance(fit_results, _FitResult): fit_results = [fit_results] elif isinstance(fit_results, list): if not all(isinstance(i, _FitResult) for i in fit_results): raise TypeError("can only accept a FitResult or a list of FitResults") else: raise TypeError("can only accept a FitResult or a list of FitResults") elements = [] if not isinstance(figure_style, str): figure_style = gl.get_default_style() for i, fr in enumerate(fit_results): di = Curve( fr.photon_nbr, fr.internal_loss, label="{:.3f} {}".format( np.mean(fr.f_r) / FREQ_UNIT_CONVERSION[freq_unit], freq_unit ), color=gl.get_color(figure_style, i), ) elements.append(di) if show_deltac: dc = Curve( fr.photon_nbr, fr.coupling_loss, line_style="--", color=gl.get_color(figure_style, i), ) elements.append(dc) y_label = r"$\delta_i, \delta_c$" if show_deltac else r"$\delta_i$" fig = _as_function_of_photon_nbr( elements=elements, y_label=y_label, figure_style=figure_style, title=title, ) return fig
def _frequency_ratio(Bll: NDArray, thetaB: float) -> NDArray: """ Fitting function for variation of resonant frequency as a function of parallel magnetic field. """ t = 100e-9 w = 25e-6 Tc = 12.5 D = 2e-5 return ( -np.pi / 48 * (e**2 * t**2) / (hbar * k * Tc) * D * (1 + thetaB**2 * w**2 / t**2) * Bll**2 )
[docs] def plot_magnetic_field( fit_result: _FitResult, two_way_sweep: bool = True, show_frequency: bool = True, fit_frequency: bool = False, title: Optional[str] = None, figure_style: str | Inherit = INHERIT, ) -> SmartFigure: r""" Plots the internal quality factor (:math:`Q_i`) as a function of the parallel magnetic field (:math:`B_\parallel`). Also can plot and fit the frequency shift induced by the magnetic field. Parameters ---------- fit_result : FitResult or list of FitResult Single or list of FitResult from a Fitter. two_way_sweep : bool, optional Whether or not the data was taken while the field was going up **and** down. Defaults to ``True``. show_frequency : bool, optional Whether or not to show the frequency variation on the plot. Defaults to ``True``. fit_frequency : bool, optional Whether or not to fit the frequency variation. Defaults to ``False``. title : str, optional Title of the figure. Defaults to ``None``. figure_style : str, optional GraphingLib figure style to apply to the plot. See `here <https://www.graphinglib.org/doc-1.5.0/handbook/figure_style_file.html#graphinglib-styles-showcase>`_ for more info. Notes ----- The frequency is fitted using the model .. math:: \frac{\Delta f}{f_r} = -\frac{\pi}{48}\frac{e^2t^2}{\hbar k_B T_c}D\left(1+\theta_B^2\frac{w^2}{t^2}\right)B_\parallel^2 from C. Roy, S. Frasca and P. Scarlino, *Magnetic-field-resilient high-impedance high-kinetic-inductance superconducting resonators*, Phys. Rev. Appl. **25**, 014069 (2026). """ if not isinstance(fit_result, _FitResult): raise TypeError("can only accept a single FitResult") if figure_style == "default": figure_style = gl.get_default_style() deltaf_over_f0 = (fit_result.f_r - fit_result.f_r[0]) / fit_result.f_r[0] qi_curves = [] if two_way_sweep: max_idx = np.where(fit_result.magnet_field == np.max(fit_result.magnet_field))[ 0 ][0] qi_to = Curve( fit_result.magnet_field[: max_idx + 1], fit_result.Q_i[: max_idx + 1], color="tab:blue", label=r"${}\to{}$T".format( fit_result.magnet_field[0], fit_result.magnet_field[max_idx] ), ) qi_back = Curve( fit_result.magnet_field[max_idx:], fit_result.Q_i[max_idx:], color="tab:blue", line_style="--", label=r"${}\to{}$T".format( fit_result.magnet_field[max_idx], fit_result.magnet_field[-1] ), ) qi_curves.append(qi_to) qi_curves.append(qi_back) deltaf = Curve( fit_result.magnet_field[: max_idx + 1], deltaf_over_f0[: max_idx + 1] * 100, color="firebrick", ) else: qi = Curve(fit_result.magnet_field, fit_result.Q_i, color="tab:blue") qi_curves.append(qi) deltaf = Curve(fit_result.magnet_field, deltaf_over_f0 * 100, color="firebrick") fig = SmartFigure( x_label=r"$B_\parallel$", y_label="$Q_i$", log_scale_y=True, title=title, figure_style=figure_style, elements=qi_curves, ) if show_frequency: fig.set_tick_params( axis="y", which="both", label_color="tab:blue", color="tab:blue" ) fig.set_visual_params(y_axis_label_color="tab:blue") twin_y = fig.create_twin_axis( is_y=True, label=r"$\Delta f / f_r$ (%)", elements=[deltaf] ) twin_y.set_tick_params(which="both", color="firebrick", label_color="firebrick") twin_y.set_visual_params(label_color="firebrick") if show_frequency and fit_frequency: fit = gl.FitFromFunction( _frequency_ratio, deltaf, color="firebrick", line_style=":" ) twin_y.add_elements(fit) return fig