Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .parameters import Parameters
from .analysis import process_raw_dataset, fit_raw_data, log_fitted_results
from .plotting import plot_target_data, plot_control_data, plot_domain_frequency

__all__ = [
"Parameters",
"process_raw_dataset",
"fit_raw_data",
"log_fitted_results",
"plot_target_data",
"plot_control_data",
"plot_domain_frequency"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import logging
from dataclasses import dataclass
from typing import Dict, Tuple

import numpy as np
import xarray as xr
from scipy.fft import fft

from qualibrate import QualibrationNode

@dataclass
class FitParameters:
"""Stores the relevant qubit spectroscopy experiment fit parameters for a single qubit"""

success: bool
interaction_max: int
coupler_flux_pulse: float
coupler_flux_min:float

def log_fitted_results(fit_results: Dict, log_callable=None):
"""
Logs the node-specific fitted results for all qubits from the fit xarray Dataset.

Parameters:
-----------
ds : xr.Dataset
Dataset containing the fitted results for all qubits.
log_callable : callable, optional
Callable for logging the fitted results. If None, a default logger is used.
"""
if log_callable is None:
log_callable = logging.getLogger(__name__).info
pass


def process_raw_dataset(ds: xr.Dataset, node: QualibrationNode):
"""
Parameters:
-----------
ds : xr.Dataset
Dataset containing the processed data.
node : QualibrationNode
The calibration node containing parameters and qubit pairs.

Returns:
--------
Tuple[xr.Dataset, Dict[str, FitResults]]
Dataset with fit results and dictionary of fit results for each qubit pair.
"""
qubit_pairs = [node.machine.qubit_pairs[pair] for pair in node.parameters.qubit_pairs]
fluxes_coupler = ds.flux_coupler.values

ds = ds.assign_coords(idle_time = ds.idle_time * 4)
if node.parameters.use_state_discrimination:
ds = ds.assign({"res_sum" : ds.state_control - ds.state_target})
else:
ds = ds.assign({"res_sum" : ds.I_control - ds.I_target})
flux_coupler_full = np.array([fluxes_coupler + qp.coupler.decouple_offset for qp in qubit_pairs])
ds = ds.assign_coords({"flux_coupler_full": (["qubit_pair", "flux_coupler"], flux_coupler_full)})

# Add the dominant frequencies to the dataset
ds['dominant_frequency'] = extract_dominant_frequencies(ds.res_sum)
ds.dominant_frequency.attrs['units'] = 'GHz'

return ds


def fit_raw_data(ds: xr.Dataset, node: QualibrationNode) -> Tuple[xr.Dataset, dict[str, FitParameters]]:
"""
Fit the qubit frequency and FWHM for each qubit in the dataset.

Parameters:
-----------
ds : xr.Dataset
Dataset containing the raw data.
node_parameters : Parameters
Parameters related to the node, including whether state discrimination is used.

Returns:
--------
xr.Dataset
Dataset containing the fit results.
"""

# Extract the relevant fitted parameters
fit_data, fit_results = _extract_relevant_fit_parameters(ds, node)
return fit_data, fit_results


def _extract_relevant_fit_parameters(fit: xr.Dataset, node: QualibrationNode):
"""Add metadata to the dataset and fit results."""

# Populate the FitParameters class with fitted values
fit_results = {}
for qp in node.machine.qubit_pairs.values():
target_gate_time = 50 # ns (fallback)
if node.parameters.cz_or_iswap == "cz" and "Cz" in qp.macros:
target_gate_time = qp.macros["Cz"].coupler_flux_pulse.length
max_frequency = 1 / (2 * target_gate_time) # in GHz
interaction_max = (fit.dominant_frequency * (fit.dominant_frequency < max_frequency)).max(dim='flux_coupler')
coupler_flux_pulse = fit.flux_coupler.isel(flux_coupler=(fit.dominant_frequency * (fit.dominant_frequency < max_frequency)).argmax(dim='flux_coupler'))
coupler_flux_min = fit.flux_coupler_full.isel(flux_coupler=fit.dominant_frequency.argmin(dim='flux_coupler'))

fit_results[qp.name] = FitParameters(
success=True,
interaction_max= int(interaction_max.values),
coupler_flux_pulse = float(coupler_flux_pulse.values),
coupler_flux_min=float(coupler_flux_min.values)
)
return fit, fit_results


def extract_dominant_frequencies(da, dim="idle_time"):
def extract_dominant_frequency(signal, sample_rate):
fft_result = fft(signal)
frequencies = np.fft.fftfreq(len(signal), 1 / sample_rate)
positive_freq_idx = np.where(frequencies > 0)
dominant_idx = np.argmax(np.abs(fft_result[positive_freq_idx]))
return frequencies[positive_freq_idx][dominant_idx]

def extract_dominant_frequency_wrapper(signal):
sample_rate = 1 / (da.coords[dim][1].values - da.coords[dim][0].values) # Assuming uniform sampling
return extract_dominant_frequency(signal, sample_rate)

dominant_frequencies = xr.apply_ufunc(
extract_dominant_frequency_wrapper, da, input_core_dims=[[dim]], output_core_dims=[[]], vectorize=True
)

return dominant_frequencies
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Literal, Optional, List

from qualibrate import NodeParameters
from qualibrate.parameters import RunnableParameters
from qualibration_libs.parameters import QubitsExperimentNodeParameters, CommonNodeParameters


class NodeSpecificParameters(RunnableParameters):
qubit_pairs: Optional[List[str]] = ["qA1-qA2"]
num_averages: int = 500
# coupler_q1_q2:
coupler_flux_min : float = -0.1
coupler_flux_max : float = 0.1
coupler_flux_step : float = 0.005
idle_time_min : int = 16
idle_time_max : int = 5000
idle_time_step : int = 4
cz_or_iswap: Literal["cz", "iswap"] = "cz"
use_saved_detuning: bool = False
flux_point_joint_or_independent_or_pairwise: Literal["joint", "independent", "pairwise"] = "joint"

class Parameters(
NodeParameters,
CommonNodeParameters,
NodeSpecificParameters,
QubitsExperimentNodeParameters,
):
pass

Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from typing import List
import xarray as xr
from matplotlib.axes import Axes
import matplotlib.pyplot as plt
import numpy as np

from qualang_tools.units import unit
from quam_builder.architecture.superconducting.qubit import AnyTransmon
from qualibration_libs.plotting import QubitGrid, grid_iter
from qualibrate import QualibrationNode

u = unit(coerce_to_integer=True)

def plot_control_data(ds: xr.Dataset, qubit_pairs: List[AnyTransmon], fits: xr.Dataset,node:QualibrationNode):
"""
Plots the resonator spectroscopy amplitude IQ_abs with fitted curves for the given qubits.

Parameters
----------
ds : xr.Dataset
The dataset containing the quadrature data.
qubits : list of AnyTransmon
A list of qubits to plot.
fits : xr.Dataset
The dataset containing the fit parameters.

Returns
-------
Figure
The matplotlib figure object containing the plots.

Notes
-----
- The function creates a grid of subplots, one for each qubit.
- Each subplot contains the raw data and the fitted curve.
"""
fig, axs = plt.subplots(nrows=len(qubit_pairs), ncols=1, figsize=(15, 9))
for ii, qp in enumerate(qubit_pairs):
ax = axs[ii] if len(qubit_pairs) > 1 else axs

if node.parameters.use_state_discrimination:
values_to_plot = ds.state_control.sel(qubit_pair=qp.name)
else:
values_to_plot = ds.Q_control.sel(qubit_pair=qp.name)
values_to_plot.plot(ax = ax, cmap = 'viridis', y = 'idle_time', x = 'flux_coupler')
qubit_pair = node.machine.qubit_pairs[qp.name]
ax.set_title(f"{qp.name}, coupler set point: {qubit_pair.coupler.decouple_offset}", fontsize = 10)
fig.suptitle('Control')
fig.tight_layout()

return fig


def plot_target_data(ds: xr.Dataset, qubit_pairs: List[AnyTransmon], fits: xr.Dataset,node:QualibrationNode):
"""
Plots the resonator spectroscopy amplitude IQ_abs with fitted curves for the given qubits.

Parameters
----------
ds : xr.Dataset
The dataset containing the quadrature data.
qubits : list of AnyTransmon
A list of qubits to plot.
fits : xr.Dataset
The dataset containing the fit parameters.

Returns
-------
Figure
The matplotlib figure object containing the plots.

Notes
-----
- The function creates a grid of subplots, one for each qubit.
- Each subplot contains the raw data and the fitted curve.
"""
fig, axs = plt.subplots(nrows=len(qubit_pairs), ncols=1, figsize=(15, 9))
for ii, qp in enumerate(qubit_pairs):
ax = axs[ii] if len(qubit_pairs) > 1 else axs

if node.parameters.use_state_discrimination:
values_to_plot = ds.state_target.sel(qubit_pair=qp.name)
else:
values_to_plot = ds.Q_target.sel(qubit_pair=qp.name)
values_to_plot.plot(ax = ax, cmap = 'viridis', y = 'idle_time', x = 'flux_coupler')
qubit_pair = node.machine.qubit_pairs[qp.name]
ax.set_title(f"{qp.name}, coupler set point: {qubit_pair.coupler.decouple_offset}", fontsize = 10)
fig.suptitle('Target')
fig.tight_layout()

return fig

def plot_domain_frequency(ds: xr.Dataset, qubit_pairs: List[AnyTransmon], fits: xr.Dataset,node:QualibrationNode):
fig, axs = plt.subplots(nrows=len(qubit_pairs), ncols=1, figsize=(15, 9))

for ii, qp in enumerate(qubit_pairs):
ax = axs[ii] if len(qubit_pairs) > 1 else axs
target_gate_time = 50 # ns (fallback)
if node.parameters.cz_or_iswap == "cz" and "Cz" in qp.macros:
target_gate_time = qp.macros["Cz"].coupler_flux_pulse.length
(1e3*ds.dominant_frequency.sel(qubit_pair=qp.name)).plot(ax = ax, marker = '.', ls = 'None', x = 'flux_coupler')
qubit_pair = node.machine.qubit_pairs[qp.name]
ax.axvline(x = qubit_pair.coupler.decouple_offset, color = 'black', label="Decouple offset")
ax.axvline(x = fits[qp.name]["coupler_flux_pulse"], color = 'red', lw = 0.5, ls = '--', label=f"Optimal frequency for {target_gate_time}ns pulse")
ax.axvline(x = fits[qp.name]["coupler_flux_min"] - qubit_pair.coupler.decouple_offset, color = 'green', lw = 0.5, ls = '--')
ax.set_title(f"{qp.name}, coupler set point: {qubit_pair.coupler.decouple_offset}", fontsize = 10)
ax.set_xlabel('Flux Coupler')
ax.set_ylabel('Frequency (MHz)')
ax.legend()
fig.suptitle('Target')
fig.tight_layout()

return fig


def plot_individual_data_with(ax: Axes, ds: xr.Dataset, qubit_pair: str, fit: xr.Dataset = None):
"""
Plots individual qubit data on a given axis with optional fit.

Parameters
----------
ax : matplotlib.axes.Axes
The axis on which to plot the data.
ds : xr.Dataset
The dataset containing the quadrature data.
qubit : dict[str, str]
mapping to the qubit to plot.
fit : xr.Dataset, optional
The dataset containing the fit parameters (default is None).

Notes
-----
- If the fit dataset is provided, the fitted curve is plotted along with the raw data.
"""

if hasattr(ds, "state_target"):
# If the dataset has 'state_target', use it for plotting
data = ds.state_target.sel(qubit_pair=qubit_pair)
else:
data = ds.I_target.sel(qubit_pair=qubit_pair)

data.assign_coords({"qubit_flux_mV": 1e3*data.qubit_flux_full, "coupler_flux_mV": 1e3*data.coupler_flux_full}).plot(x='qubit_flux_mV',y='coupler_flux_mV')

# Only plot fit results if they exist and are valid
if fit is not None:
try:
# Check if fit values exist and are valid (not NaN)
qubit_flux_max = float(fit['qubit_flux_max'])
coupler_flux_min = float(fit['coupler_flux_min'])
if not (np.isnan(qubit_flux_max) or np.isnan(coupler_flux_min)):
ax.axhline(1e3*coupler_flux_min, color = 'red', lw = 0.5, ls = '--')
#ax.axhline(1e3*machine.qubit_pairs[qp['qubit']].coupler.decouple_offset, color = 'blue', lw =0.5, ls = '--')
ax.axvline(1e3*qubit_flux_max, color = 'red', lw =0.5, ls = '--')
else:
ax.set_title(f"{qubit_pair} - Fit Failed (Invalid Parameters)")
except (ValueError, TypeError, AttributeError):
ax.set_title(f"{qubit_pair} - Fit Failed")
else:
ax.set_title(f"{qubit_pair} - No Fit Data")
detuning_data = ds.sel(qubit_pair=qubit_pair).detuning.values * 1e-6
def flux_to_detuning(x):
return np.interp(x, 1e3*data.qubit_flux_full, detuning_data)

def detuning_to_flux(y):
return np.interp(y, detuning_data, 1e3*data.qubit_flux_full)

sec_ax = ax.secondary_xaxis('top', functions=(flux_to_detuning, detuning_to_flux))
sec_ax.set_xlabel('Detuning [MHz]')

ax.set_xlabel("qubit flux [mV]")
ax.set_ylabel("coupler flux [mV]")
Loading
Loading