diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 018bd7dba9..c5f17e6585 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -67,6 +67,7 @@ ~characterization.FineSXAmplitude ~characterization.Rabi ~characterization.EFRabi + ~characterization.CrossResRabi ~characterization.RamseyXY ~characterization.FineFrequency ~characterization.ReadoutAngle @@ -138,6 +139,7 @@ class instance to manage parameters and pulse schedules. ~calibration.RoughAmplitudeCal ~calibration.RoughXSXAmplitudeCal ~calibration.EFRoughXSXAmplitudeCal + ~calibration.CrossResRoughAmplitudeCal """ from .calibration import ( @@ -148,6 +150,7 @@ class instance to manage parameters and pulse schedules. RoughAmplitudeCal, RoughXSXAmplitudeCal, EFRoughXSXAmplitudeCal, + CrossResRoughAmplitudeCal, FineAmplitudeCal, FineXAmplitudeCal, FineSXAmplitudeCal, @@ -172,6 +175,7 @@ class instance to manage parameters and pulse schedules. FineSXDrag, Rabi, EFRabi, + CrossResRabi, HalfAngle, FineAmplitude, FineXAmplitude, diff --git a/qiskit_experiments/library/calibration/__init__.py b/qiskit_experiments/library/calibration/__init__.py index 8b24f93075..501492806a 100644 --- a/qiskit_experiments/library/calibration/__init__.py +++ b/qiskit_experiments/library/calibration/__init__.py @@ -48,6 +48,7 @@ RoughAmplitudeCal RoughXSXAmplitudeCal EFRoughXSXAmplitudeCal + CrossResRoughAmplitudeCal Calibrations management ======================= @@ -57,7 +58,12 @@ from .rough_frequency import RoughFrequencyCal, RoughEFFrequencyCal from .rough_drag_cal import RoughDragCal -from .rough_amplitude_cal import RoughAmplitudeCal, RoughXSXAmplitudeCal, EFRoughXSXAmplitudeCal +from .rough_amplitude_cal import ( + RoughAmplitudeCal, + RoughXSXAmplitudeCal, + EFRoughXSXAmplitudeCal, + CrossResRoughAmplitudeCal, +) from .fine_amplitude import FineAmplitudeCal, FineXAmplitudeCal, FineSXAmplitudeCal from .fine_drag_cal import FineDragCal, FineXDragCal, FineSXDragCal from .frequency_cal import FrequencyCal diff --git a/qiskit_experiments/library/calibration/rough_amplitude_cal.py b/qiskit_experiments/library/calibration/rough_amplitude_cal.py index a04a13a050..84acfde429 100644 --- a/qiskit_experiments/library/calibration/rough_amplitude_cal.py +++ b/qiskit_experiments/library/calibration/rough_amplitude_cal.py @@ -22,7 +22,7 @@ from qiskit_experiments.framework import ExperimentData from qiskit_experiments.calibration_management import BaseCalibrationExperiment, Calibrations -from qiskit_experiments.library.characterization import Rabi +from qiskit_experiments.library.characterization import Rabi, CrossResRabi from qiskit_experiments.calibration_management.update_library import BaseUpdater from qiskit_experiments.warnings import qubit_deprecate @@ -288,3 +288,93 @@ def _attach_calibrations(self, circuit: QuantumCircuit): if self._cals.has_template("x", self.physical_qubits): schedule = self._cals.get_schedule("x", self.physical_qubits) circuit.add_calibration("x", self.physical_qubits, schedule) + + +class CrossResRoughAmplitudeCal(BaseCalibrationExperiment, CrossResRabi): + """A calibration version of the CrossResRabi experiment.""" + + def __init__( + self, + physical_qubits: Sequence[int], + calibrations: Calibrations, + schedule_name: str = "ecr", + cal_parameter_name: str = "amp", + auto_update: bool = True, + group: str = "default", + backend: Optional[Backend] = None, + ): + """Create new experiment. + + Args: + physical_qubits: Two element sequence of control and target qubit index. + calibrations: The calibrations instance with the schedules. + schedule_name: The name of the schedule to calibrate. + cal_parameter_name: The name of the parameter in the schedule to update. + auto_update: Whether or not to automatically update the calibrations. + group: The group of calibration parameters to use. + backend: Optional, the backend to run the experiment on. + """ + schedule = calibrations.get_schedule( + name=schedule_name, + qubits=tuple(physical_qubits), + assign_params={cal_parameter_name: CrossResRabi.parameter}, + group=group, + ) + super().__init__( + calibrations, + physical_qubits, + schedule=schedule, + backend=backend, + cal_parameter_name=cal_parameter_name, + auto_update=auto_update, + ) + self.set_experiment_options( + angle_schedules=[ + AnglesSchedules( + target_angle=np.pi / 2, + parameter=cal_parameter_name, + schedule=schedule_name, + previous_value=None, + ), + ] + ) + + @classmethod + def _default_experiment_options(cls): + """Default experiment options. + + Experiment Options: + angle_schedules (list[AngleSchedules]): A list of parameter value + information. Each entry of the list is a tuple of + the target angle, the name of parameter to update, + the name of associated schedule to update, and previous parameter value. + """ + options = super()._default_experiment_options() + options.update_options(angle_schedules=None) + return options + + def _metadata(self): + metadata = super()._metadata() + metadata["angles_schedules"] = self.experiment_options.angle_schedules + + return metadata + + def _attach_calibrations(self, circuit: QuantumCircuit): + pass + + def update_calibrations(self, experiment_data: ExperimentData): + rabi_rate_o1 = 2 * np.pi * BaseUpdater.get_value(experiment_data, "cross_res_rabi_rate_o1") + rabi_rate_o3 = 2 * np.pi * BaseUpdater.get_value(experiment_data, "cross_res_rabi_rate_o3") + group = experiment_data.metadata["cal_group"] + + for angle, param, schedule, _ in experiment_data.metadata["angles_schedules"]: + candidates = np.roots([rabi_rate_o3, 0.0, rabi_rate_o1, -angle]) + value = min(candidates[candidates > 0]) + BaseUpdater.add_parameter_value( + self._cals, + experiment_data, + value, + param, + schedule, + group, + ) diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index df9024c62d..f91f5d2e17 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -32,6 +32,7 @@ EchoedCrossResonanceHamiltonian Rabi EFRabi + CrossResRabi HalfAngle FineAmplitude FineXAmplitude @@ -64,6 +65,7 @@ T2RamseyAnalysis T2HahnAnalysis TphiAnalysis + CrossResRabiAnalysis CrossResonanceHamiltonianAnalysis DragCalAnalysis FineAmplitudeAnalysis @@ -86,6 +88,7 @@ T1KerneledAnalysis, T2HahnAnalysis, TphiAnalysis, + CrossResRabiAnalysis, CrossResonanceHamiltonianAnalysis, ReadoutAngleAnalysis, ResonatorSpectroscopyAnalysis, @@ -102,7 +105,7 @@ from .t2hahn import T2Hahn from .tphi import Tphi from .cr_hamiltonian import CrossResonanceHamiltonian, EchoedCrossResonanceHamiltonian -from .rabi import Rabi, EFRabi +from .rabi import Rabi, EFRabi, CrossResRabi from .half_angle import HalfAngle from .fine_amplitude import FineAmplitude, FineXAmplitude, FineSXAmplitude, FineZXAmplitude from .ramsey_xy import RamseyXY, StarkRamseyXY diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index cfb26861dc..26990c14d3 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -20,6 +20,7 @@ from .t1_analysis import T1Analysis from .t1_analysis import T1KerneledAnalysis from .tphi_analysis import TphiAnalysis +from .cr_rabi_analysis import CrossResRabiAnalysis from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis from .readout_angle_analysis import ReadoutAngleAnalysis from .local_readout_error_analysis import LocalReadoutErrorAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/cr_rabi_analysis.py b/qiskit_experiments/library/characterization/analysis/cr_rabi_analysis.py new file mode 100644 index 0000000000..5a6ab97ab7 --- /dev/null +++ b/qiskit_experiments/library/characterization/analysis/cr_rabi_analysis.py @@ -0,0 +1,147 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Analyze oscillating data of cross resonance Rabi.""" + + +from typing import List, Union + +import lmfit +import numpy as np + +from qiskit_experiments.framework import Options +import qiskit_experiments.curve_analysis as curve + + +class CrossResRabiAnalysis(curve.CurveAnalysis): + r"""Cross resonance Rabi oscillation analysis class with nonlinear frequency. + + # section: fit_model + + Under the perturbation approximation, the amplitude dependence of + the controlled rotation term, i.e. :math:`ZX` term, + in the cross resonance Hamiltonian might be fit by [1] + + .. math:: + + y = {\rm amp} \cos\left( + 2 \pi\cdot \left( {\rm freq}^{o1} \cdot x + {\rm freq}^{o3} \cdot x^3 \right) + \pi + \right) + {\rm base} + + This approximation is valid as long as the tone amplitude is sufficiently weaker + than the breakdown point at :math:`\Omega/\Delta \ll 1` where + :math:`\Omega` is the tone amplitude and :math:`\Delta` is the qubit-qubit detuning. + + # section: fit_parameters + defpar \rm amp: + desc: Amplitude of the oscillation. + init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. + bounds: [0, 1] + + defpar \rm base: + desc: Base line. + init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.\ + guess.constant_sinusoidal_offset`. + bounds: [-1, 1] + + defpar \rm freq_o1: + desc: Frequency of the oscillation in the first order. + This is the fit parameter of interest. + init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.frequency`. + bounds: [0, inf]. + + defpar \rm freq_o3: + desc: Frequency of the oscillation in the third order. + This is the fit parameter of interest. + init_guess: 0.0. + bounds: [0, inf]. + + # section: reference + .. ref_arxiv:: 1 1804.04073 + + """ + + def __init__(self): + super().__init__( + models=[ + lmfit.models.ExpressionModel( + expr="amp * cos(2 * pi * (freq_o1 * x + freq_o3 * x**3) + pi) + offset", + name="cos", + ) + ], + ) + + @classmethod + def _default_options(cls) -> Options: + options = super()._default_options() + options.update_options( + result_parameters=[ + curve.ParameterRepr("freq_o1", "cross_res_rabi_rate_o1"), + curve.ParameterRepr("freq_o3", "cross_res_rabi_rate_o3"), + ], + outcome="1", + ) + options.plotter.set_figure_options(xlabel="Amplitude", ylabel="Target P(1)") + return options + + def _generate_fit_guesses( + self, + user_opt: curve.FitOptions, + curve_data: curve.CurveData, + ) -> Union[curve.FitOptions, List[curve.FitOptions]]: + """Create algorithmic initial fit guess from analysis options and curve data. + + Args: + user_opt: Fit options filled with user provided guess and bounds. + curve_data: Formatted data collection to fit. + + Returns: + List of fit options that are passed to the fitter function. + """ + y_offset = curve.guess.constant_sinusoidal_offset(curve_data.y) + + user_opt.bounds.set_if_empty( + amp=(0, 1), + freq_o1=(0, np.inf), + freq_o3=(-np.inf, np.inf), + offset=[-1, 1], + ) + user_opt.p0.set_if_empty( + offset=y_offset, + ) + user_opt.p0.set_if_empty( + freq_o1=curve.guess.frequency(curve_data.x, curve_data.y - user_opt.p0["offset"]), + freq_o3=0.0, + amp=curve.guess.max_height(curve_data.y - user_opt.p0["offset"], absolute=True)[0], + ) + + return user_opt + + def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: + """Algorithmic criteria for whether the fit is good or bad. + + A good fit has: + - more than a quarter of a full period, + - an error on the fit frequency lower than the fit frequency. + """ + fit_freq_o1 = fit_data.ufloat_params["freq_o1"] + fit_freq_o3 = fit_data.ufloat_params["freq_o3"] + + criteria = [ + curve.utils.is_error_not_significant(fit_freq_o1), + curve.utils.is_error_not_significant(fit_freq_o3), + ] + + if all(criteria): + return "good" + + return "bad" diff --git a/qiskit_experiments/library/characterization/rabi.py b/qiskit_experiments/library/characterization/rabi.py index 2b2d73ae10..7d69f0c37b 100644 --- a/qiskit_experiments/library/characterization/rabi.py +++ b/qiskit_experiments/library/characterization/rabi.py @@ -26,6 +26,7 @@ from qiskit_experiments.framework.restless_mixin import RestlessMixin from qiskit_experiments.curve_analysis import ParameterRepr, OscillationAnalysis from qiskit_experiments.warnings import qubit_deprecate +from qiskit_experiments.library.characterization import CrossResRabiAnalysis class Rabi(BaseExperiment, RestlessMixin): @@ -112,6 +113,7 @@ def __init__( self.analysis.set_options( result_parameters=[ParameterRepr("freq", self.__outcome__)], normalization=True, + ) self.analysis.plotter.set_figure_options( xlabel="Amplitude", @@ -215,3 +217,158 @@ def _pre_circuit(self) -> QuantumCircuit: circ = QuantumCircuit(1) circ.x(0) return circ + + +class CrossResRabi(BaseExperiment): + """An experiment that scans a cross resonance amplitude to calibrate rotations angle. + + # section: overview + + This experiment takes a user provided schedule for cross resonance, + which is attached to experiment circuits. + + .. parsed-literal:: + + ┌──────────────────────────┐ + q_0: ┤0 ├─── + │ CrossResRabi(amplitude) │┌─┐ + q_1: ┤1 ├┤M├ + └──────────────────────────┘└╥┘ + c: 1/═════════════════════════════╩═ + 0 + + This experiment measures only target qubit. + + # section: example + + User can build parameterized schedule and provide as follows: + + .. code-block:: python + + from qiskit_experiments.library import CrossResRabi + from qiskit import pulse + from qiskit.providers.fake_provider import FakeHanoiV2 + from math import pi + + amp_param = CrossResRabi.parameter + backend = FakeHanoiV2() + + with pulse.build(default_alignment="sequential") as ecr: + pulse.play( + pulse.GaussianSquare( + 800, amp_param, 64, risefall_sigma_ratio=2, angle=0 + ), + pulse.ControlChannel(0), + ) + pulse.call(backend.target["x"][(0, )].calibration) + pulse.play( + pulse.GaussianSquare( + 800, amp_param, 64, risefall_sigma_ratio=2, angle=pi + ), + pulse.ControlChannel(0), + ) + + exp = CrossResRabi((0,), backend, schedule=ecr) + + Note that this example code attaches an echoed cross resonance sequence to the experiment. + The sign of controlled rotation terms (ZX, ZY) is control qubit state dependent, + and it may constructively or destructively interfere with local terms (IX, IY). + Since the echo sequence eliminates the local terms, above example code may give you + state independent Rabi rate. + + # section: analysis_ref + :class:`~qiskit_experiments.curve_analysis.OscillationAnalysis` + + """ + + parameter = Parameter("amplitude") + """Qiskit Parameter object used to create parameterized circuits.""" + + def __init__( + self, + physical_qubits: Sequence[int], + backend: Optional[Backend] = None, + **experiment_options, + ): + """Create new experiment. + + Args: + physical_qubits: Two element sequence of control and target qubit index. + backend: Optional, the backend to run the experiment on. + experiment_options: See ``self.default_experiment_options`` for details. + """ + super().__init__( + physical_qubits=physical_qubits, + analysis=CrossResRabiAnalysis(), + backend=backend, + ) + if experiment_options: + self.set_experiment_options(**experiment_options) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + amplitudes (list[float]): The list of amplitudes that will be scanned + in the experiment. If not set, then ``num_amplitudes`` + evenly spaced amplitudes between ``min_amplitude`` and + ``max_amplitude`` are used. If ``amplitudes`` is set, + these experiment options are ignored. + min_amplitude (float): Minimum amplitude to use. + max_amplitude (float): Maximum amplitude to use. + num_amplitudes (int): Number of circuits to use for parameter scan. + schedule (ScheduleBlock): The Schedule that will be used in the + experiment. This schedule must be parameterized with the + parameter object ``CrossResRabi.parameter``. + """ + options = super()._default_experiment_options() + options.update_options( + amplitudes=None, + min_amplitude=-0.7, + max_amplitude=0.7, + num_amplitudes=51, + schedule=None, + ) + return options + + def parameters(self) -> np.ndarray: + """Return parameters to scan.""" + opt = self.experiment_options + + if self.experiment_options.amplitudes: + return np.asarray(opt.amplitudes, dtype=float) + return np.linspace(opt.min_amplitude, opt.max_amplitude, opt.num_amplitudes) + + def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: + """Return parameterized circuits.""" + opt = self.experiment_options + cr_rabi_gate = Gate("CrossResRabi", 2, [self.parameter]) + + if opt.schedule is None: + raise RuntimeError("Cross resonance schedule is not set in the experiment options.") + + if self.parameter not in opt.schedule.parameters: + raise RuntimeError( + f"Specified schedule {opt.schedule.name} might be parameterized with " + "parameter objects that this experiment cannot recognize. " + f"Please build your schedule with '{self.__class__.__name__}.parameter.'" + ) + + circuit = QuantumCircuit(2, 1) + circuit.append(cr_rabi_gate, [0, 1]) + circuit.measure(1, 0) + circuit.add_calibration(cr_rabi_gate, self.physical_qubits, opt.schedule) + + return (circuit,) + + def circuits(self) -> List[QuantumCircuit]: + temp_circ = self.parameterized_circuits()[0] + + circs = [] + for param in self.parameters(): + assigned = temp_circ.assign_parameters({self.parameter: param}, inplace=False) + assigned.metadata = {"xval": param} + circs.append(assigned) + + return circs diff --git a/releasenotes/notes/add-rough-cr-amp-e666ded368737b04.yaml b/releasenotes/notes/add-rough-cr-amp-e666ded368737b04.yaml new file mode 100644 index 0000000000..0790c8b99e --- /dev/null +++ b/releasenotes/notes/add-rough-cr-amp-e666ded368737b04.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add characterization and calibration experiment for cross resonance amplitude, + :class:`.CrossResRabi` and :class:`.CrossResRoughAmplitudeCal`, respectively. diff --git a/test/library/calibration/test_rabi.py b/test/library/calibration/test_rabi.py index 14ad13a31a..73e6a94f35 100644 --- a/test/library/calibration/test_rabi.py +++ b/test/library/calibration/test_rabi.py @@ -22,7 +22,7 @@ from qiskit.qobj.utils import MeasLevel from qiskit_experiments.framework import ExperimentData, ParallelExperiment -from qiskit_experiments.library import Rabi, EFRabi +from qiskit_experiments.library import Rabi, EFRabi, CrossResRabi from qiskit_experiments.curve_analysis.standard_analysis.oscillation import OscillationAnalysis from qiskit_experiments.data_processing.data_processor import DataProcessor @@ -163,6 +163,41 @@ def test_roundtrip_serializable(self): self.assertRoundTripSerializable(exp) +class TestCrossResRabi(QiskitExperimentsTestCase): + """Test case for cross resonance Rabi and calibration.""" + + def test_raise_unrecognized_parameter(self): + """Test raise an error when user build schedule with own parameter object.""" + with pulse.build() as sched: + pulse.play( + pulse.Constant(duration=100, amp=Parameter("amplitude")), + pulse.ControlChannel(0), + ) + exp = CrossResRabi(physical_qubits=(0, 1), schedule=sched) + with self.assertRaises(RuntimeError): + exp.parameterized_circuits() + + def test_get_parameters(self): + """Test if parameters are expected ndarray.""" + exp = CrossResRabi( + physical_qubits=(0, 1), + min_amplitude=-0.1, + max_amplitude=0.1, + num_amplitudes=11, + ) + np.testing.assert_array_equal(exp.parameters(), np.linspace(-0.1, 0.1, 11)) + + def test_return_parameter(self): + """Test if experiment returns expected quantity.""" + exp = CrossResRabi(physical_qubits=(0, 1)) + self.assertEqual(exp.analysis.options.result_parameters[0].repr, "cross_res_rabi_rate") + + def test_end_to_end(self): + """End-to-end testing of CrossResRabi experiment.""" + # TODO write this test + pass + + class TestRabiCircuits(QiskitExperimentsTestCase): """Test the circuits generated by the experiment and the options.""" diff --git a/test/library/calibration/test_rough_amplitude.py b/test/library/calibration/test_rough_amplitude.py index 575d8ee2ac..622a51e2d4 100644 --- a/test/library/calibration/test_rough_amplitude.py +++ b/test/library/calibration/test_rough_amplitude.py @@ -175,3 +175,12 @@ def test_ef_update(self): self.assertTrue( abs(self.cals.get_parameter_value("amp", 0, "sx12") * (4 / 5) - default_amp / 2) < tol ) + + +class TestCrossResRoughAmplitudeCal(QiskitExperimentsTestCase): + """Test case for CrossResRoughAmplitudeCal experiment.""" + + def test_end_to_end(self): + """End-to-end testing of calibration experiment.""" + # TODO implement this + pass