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 matplotlib import pyplot as plt
from matplotlib import ticker, colors
from .util import convert_complex_to_magphase
from .typing import _FitResult
FREQ_UNIT_CONVERSION = {"GHz": 1e9, "MHz": 1e6, "kHz": 1e3}
deep_blue = "#000a1a"
blue = "#0099ff"
white = "#ffffff"
orange = "#ff6600"
deep_orange = "#662900"
clist = list(map(colors.to_rgb, [deep_blue, white]))
cmap = colors.LinearSegmentedColormap.from_list("cmap", clist)
div_clist = list(map(colors.to_rgb, [deep_blue, blue, white, orange, deep_orange]))
div_cmap = colors.LinearSegmentedColormap.from_list("div_cmap", div_clist)
[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 % 7),
)
elements.append(qi)
if show_Qc:
qc = Curve(
fr.photon_nbr,
fr.Q_c,
line_style="--",
color=gl.get_color(figure_style, i % 7),
)
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 % 7),
)
elements.append(di)
if show_deltac:
dc = Curve(
fr.photon_nbr,
fr.coupling_loss,
line_style="--",
color=gl.get_color(figure_style, i % 7),
)
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
[docs]
def plot_power_dep_maps(
fit_result: _FitResult,
savepath: Optional[str] = None,
additional_rcparams: Optional[dict] = None,
) -> None:
"""
Plots the magnitude as function of frequency for all input powers as a 2D map. Also shows
the difference of the data with the fitted model for each input powers and the :math:`R^2`.
This is useful to get an overview of the quality of the fits.
Parameters
----------
fit_result : FitResult
Fit result to plot.
savepath : str, optional
If specified, the figure will be saved at this path instead of being shown.
additional_rcparams : dict, optional
Additional rcParams for the figure.
Note
----
Compared to the other plotting functions, this uses matplotlib directly which is why it does
not return the figure object.
Examples
--------
.. figure:: ../images/power_dep_maps.svg
:scale: 100%
:align: center
:class: margin-bottom
"""
plt.rcParams.update(
{
"xtick.direction": "in",
"ytick.direction": "in",
"path.snap": True,
}
)
if additional_rcparams:
plt.rcParams.update(additional_rcparams)
magnitude = fit_result.source_file.s21_mag.range
normalized_mag = magnitude - magnitude[:, [0]]
Ny, Nx = normalized_mag.shape
power = (
fit_result.source_file.vna_power.range
- fit_result.source_file.variable_attenuator.range
+ fit_result.source_file.cryostat_attenuation
)
freq = fit_result.source_file.vna_frequency.range
results = fit_result._results
fit_model = np.empty(magnitude.shape)
for i, res in enumerate(results):
fit_model[i, :] = 20 * np.log10(np.abs(res.evaluate_fit(freq)))
normalized_fit_model = fit_model - fit_model[:, [0]]
diff = normalized_mag - normalized_fit_model
mean = np.mean(normalized_mag, axis=1)
RSS = np.sum(diff**2, axis=1)
TSS = np.sum(np.square(normalized_mag - mean[:, None]), axis=1)
R_squared = np.ones((Ny,)) - RSS / TSS
fig, (ax1, ax2, ax3) = plt.subplots(
1,
3,
figsize=(10.5, 5),
sharey=True,
constrained_layout=True,
width_ratios=(1, 1, 0.35),
)
x_edges = np.linspace(freq[0] / 1e9, freq[-1] / 1e9, Nx + 1)
y_edges = np.linspace(power[0], power[-1], Ny + 1)
im = ax1.pcolormesh(
x_edges,
y_edges,
normalized_mag,
cmap=cmap,
shading="flat",
edgecolors="face",
lw=0.1,
)
ax1.set_aspect("auto")
cb = plt.colorbar(im, location="top")
cb.ax.set_xlabel("Normalized $|S_{21}|$ (dB)")
imdiff = ax2.pcolormesh(
x_edges,
y_edges,
diff,
cmap=div_cmap,
shading="flat",
edgecolors="face",
lw=0.1,
norm=colors.TwoSlopeNorm(vcenter=0, vmin=diff.min(), vmax=diff.max()),
)
ax2.set_aspect("auto")
cb2 = plt.colorbar(imdiff, location="top")
cb2.ax.set_xlabel(r"$|S_{21}^\mathrm{mes}|-|S_{21}^\mathrm{mod}|$ (dB)")
ax3.plot(
R_squared,
np.linspace(power[0], power[-1], Ny),
lw=2,
marker="o",
markersize=6,
c="firebrick",
)
ax3.set_xlabel("$R^2$")
fig.supxlabel("Frequency (GHz)", va="baseline", size="medium", x=0.45, y=0.01)
ax1.set_ylabel("Power (dBm)")
ax1.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.5f"))
ax2.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.5f"))
fig.suptitle(f"Fit resonator @ {np.mean(freq) / 1e9} GHz")
if savepath:
plt.savefig(savepath)
else:
plt.show()