Source code for qibocal.protocols.qubit_spectroscopies.qubit_spectroscopy

from dataclasses import dataclass, field
from typing import Optional

import numpy as np
from qibolab import Delay, Parameter, PulseSequence, Sweeper

from qibocal.auto.operation import Parameters, QubitId, Results, Routine
from qibocal.calibration import CalibrationPlatform
from qibocal.result import magnitude, phase
from qibocal.update import replace

from ... import update
from ..resonator_spectroscopies.resonator_spectroscopy import (
    ResonatorSpectroscopyData,
    ResSpecType,
)
from ..resonator_spectroscopies.resonator_utils import spectroscopy_plot
from ..utils import (
    chi2_reduced,
    lorentzian,
    lorentzian_fit,
    readout_frequency,
)

__all__ = [
    "qubit_spectroscopy",
    "QubitSpectroscopyParameters",
    "QubitSpectroscopyResults",
    "QubitSpectroscopyData",
    "_fit",
]


[docs] @dataclass class QubitSpectroscopyParameters(Parameters): """QubitSpectroscopy runcard inputs.""" freq_width: int """Width [Hz] for frequency sweep relative to the qubit frequency.""" freq_step: int """Frequency [Hz] step for sweep.""" drive_duration: int """Drive pulse duration [ns]. Same for all qubits.""" drive_amplitude: Optional[float] = None """Drive pulse amplitude (optional). Same for all qubits.""" hardware_average: bool = True """By default hardware average will be performed."""
[docs] @dataclass class QubitSpectroscopyResults(Results): """QubitSpectroscopy outputs.""" frequency: dict[QubitId, dict[str, float]] """Drive frequecy [GHz] for each qubit.""" amplitude: dict[QubitId, float] """Input drive amplitude. Same for all qubits.""" fitted_parameters: dict[QubitId, list[float]] """Raw fitting output.""" chi2_reduced: dict[QubitId, tuple[float, Optional[float]]] = field( default_factory=dict ) """Chi2 reduced.""" error_fit_pars: dict[QubitId, list] = field(default_factory=dict) """Errors of the fit parameters."""
[docs] class QubitSpectroscopyData(ResonatorSpectroscopyData): """QubitSpectroscopy acquisition outputs."""
def _acquisition( params: QubitSpectroscopyParameters, platform: CalibrationPlatform, targets: list[QubitId], ) -> QubitSpectroscopyData: """Data acquisition for qubit spectroscopy.""" # create a sequence of pulses for the experiment: # long drive probing pulse - MZ delta_frequency_range = np.arange( -params.freq_width / 2, params.freq_width / 2, params.freq_step ) # taking advantage of multiplexing, apply the same set of gates to all qubits in parallel sequence = PulseSequence() ro_pulses = {} qd_pulses = {} amplitudes = {} sweepers = [] for qubit in targets: natives = platform.natives.single_qubit[qubit] qd_channel, qd_pulse = natives.RX()[0] ro_channel, ro_pulse = natives.MZ()[0] qd_pulse = replace(qd_pulse, duration=params.drive_duration) if params.drive_amplitude is not None: qd_pulse = replace(qd_pulse, amplitude=params.drive_amplitude) amplitudes[qubit] = qd_pulse.amplitude qd_pulses[qubit] = qd_pulse ro_pulses[qubit] = ro_pulse sequence.append((qd_channel, qd_pulse)) sequence.append((ro_channel, Delay(duration=qd_pulse.duration))) sequence.append((ro_channel, ro_pulse)) f0 = platform.config(qd_channel).frequency sweepers.append( Sweeper( parameter=Parameter.frequency, values=f0 + delta_frequency_range, channels=[qd_channel], ) ) # Create data structure for data acquisition. data = QubitSpectroscopyData( resonator_type=platform.resonator_type, amplitudes=amplitudes ) results = platform.execute( [sequence], [sweepers], updates=[ {platform.qubits[q].probe: {"frequency": readout_frequency(q, platform)}} for q in targets ], **params.execution_parameters, ) # retrieve the results for every qubit for qubit, ro_pulse in ro_pulses.items(): result = results[ro_pulse.id] # store the results f0 = platform.config(platform.qubits[qubit].drive).frequency signal = magnitude(result) _phase = phase(result) if len(signal.shape) > 1: error_signal = np.std(signal, axis=0, ddof=1) / np.sqrt(signal.shape[0]) signal = np.mean(signal, axis=0) error_phase = np.std(_phase, axis=0, ddof=1) / np.sqrt(_phase.shape[0]) _phase = np.mean(_phase, axis=0) else: error_signal, error_phase = None, None data.register_qubit( ResSpecType, (qubit), dict( signal=signal, phase=_phase, freq=delta_frequency_range + f0, error_signal=error_signal, error_phase=error_phase, ), ) return data
[docs] def _fit(data: QubitSpectroscopyData) -> QubitSpectroscopyResults: """Post-processing function for QubitSpectroscopy.""" qubits = data.qubits frequency = {} fitted_parameters = {} error_fit_pars = {} chi2 = {} for qubit in qubits: fit_result = lorentzian_fit( data[qubit], resonator_type=data.resonator_type, fit="qubit" ) if fit_result is not None: frequency[qubit], fitted_parameters[qubit], error_fit_pars[qubit] = ( fit_result ) chi2[qubit] = ( chi2_reduced( data[qubit].signal, lorentzian(data[qubit].freq, *fitted_parameters[qubit]), data[qubit].error_signal, ), np.sqrt(2 / len(data[qubit].freq)), ) return QubitSpectroscopyResults( frequency=frequency, fitted_parameters=fitted_parameters, amplitude=data.amplitudes, error_fit_pars=error_fit_pars, chi2_reduced=chi2, )
def _plot(data: QubitSpectroscopyData, target: QubitId, fit: QubitSpectroscopyResults): """Plotting function for QubitSpectroscopy.""" return spectroscopy_plot(data, target, fit) def _update( results: QubitSpectroscopyResults, platform: CalibrationPlatform, target: QubitId ): platform.calibration.single_qubits[target].qubit.frequency_01 = results.frequency[ target ] update.drive_frequency(results.frequency[target], platform, target) qubit_spectroscopy = Routine(_acquisition, _fit, _plot, _update) """QubitSpectroscopy Routine object."""