diff --git a/qiskit_experiments/characterization/__init__.py b/qiskit_experiments/characterization/__init__.py index 5478423c39..61927fd296 100644 --- a/qiskit_experiments/characterization/__init__.py +++ b/qiskit_experiments/characterization/__init__.py @@ -39,4 +39,5 @@ """ from .t1 import T1, T1Analysis from .qubit_spectroscopy import QubitSpectroscopy, SpectroscopyAnalysis +from .ef_spectroscopy import EFSpectroscopy from .t2star_experiment import T2StarExperiment, T2StarAnalysis diff --git a/qiskit_experiments/characterization/ef_spectroscopy.py b/qiskit_experiments/characterization/ef_spectroscopy.py new file mode 100644 index 0000000000..8e32d11253 --- /dev/null +++ b/qiskit_experiments/characterization/ef_spectroscopy.py @@ -0,0 +1,43 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Spectroscopy for the e-f transition.""" + +from qiskit import QuantumCircuit +from qiskit.circuit import Gate + +from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy + + +class EFSpectroscopy(QubitSpectroscopy): + """Class that runs spectroscopy on the e-f transition by scanning the frequency. + + The circuits produced by spectroscopy, i.e. + + .. parsed-literal:: + + ┌───┐┌────────────┐ ░ ┌─┐ + q_0: ┤ X ├┤ Spec(freq) ├─░─┤M├ + └───┘└────────────┘ ░ └╥┘ + measure: 1/═══════════════════════╩═ + 0 + + """ + + def _template_circuit(self, freq_param) -> QuantumCircuit: + """Return the template quantum circuit.""" + circuit = QuantumCircuit(1) + circuit.x(0) + circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) + circuit.measure_active() + + return circuit diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index d22f9dc38b..15a616a5e9 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -12,14 +12,15 @@ """Spectroscopy experiment class.""" -from typing import List, Dict, Any, Union, Optional +from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np -import qiskit.pulse as pulse from qiskit import QuantumCircuit from qiskit.circuit import Gate, Parameter from qiskit.exceptions import QiskitError from qiskit.providers import Backend +import qiskit.pulse as pulse +from qiskit.utils import apply_prefix from qiskit.providers.options import Options from qiskit.qobj.utils import MeasLevel @@ -195,9 +196,7 @@ class QubitSpectroscopy(BaseExperiment): """ __analysis_class__ = SpectroscopyAnalysis - - # Supported units for spectroscopy. - __units__ = {"Hz": 1.0, "kHz": 1.0e3, "MHz": 1.0e6, "GHz": 1.0e9} + __spec_gate_name__ = "Spec" @classmethod def _default_run_options(cls) -> Options: @@ -249,14 +248,49 @@ def __init__( if len(frequencies) < 3: raise QiskitError("Spectroscopy requires at least three frequencies.") - if unit not in self.__units__: - raise QiskitError(f"Unsupported unit: {unit}.") + if unit == "Hz": + self._frequencies = frequencies + else: + self._frequencies = [apply_prefix(freq, unit) for freq in frequencies] super().__init__([qubit]) - self._frequencies = [freq * self.__units__[unit] for freq in frequencies] self._absolute = absolute - self.set_analysis_options(xlabel=f"Frequency [{unit}]", ylabel="Signal [arb. unit]") + + if not self._absolute: + self.set_analysis_options(xlabel="Frequency shift [Hz]") + else: + self.set_analysis_options(xlabel="Frequency [Hz]") + + self.set_analysis_options(ylabel="Signal [arb. unit]") + + def _spec_gate_schedule( + self, backend: Optional[Backend] = None + ) -> Tuple[pulse.ScheduleBlock, Parameter]: + """Create the spectroscopy schedule.""" + freq_param = Parameter("frequency") + with pulse.build(backend=backend, name="spectroscopy") as schedule: + pulse.shift_frequency(freq_param, pulse.DriveChannel(self.physical_qubits[0])) + pulse.play( + pulse.GaussianSquare( + duration=self.experiment_options.duration, + amp=self.experiment_options.amp, + sigma=self.experiment_options.sigma, + width=self.experiment_options.width, + ), + pulse.DriveChannel(self.physical_qubits[0]), + ) + pulse.shift_frequency(-freq_param, pulse.DriveChannel(self.physical_qubits[0])) + + return schedule, freq_param + + def _template_circuit(self, freq_param) -> QuantumCircuit: + """Return the template quantum circuit.""" + circuit = QuantumCircuit(1) + circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) + circuit.measure_active() + + return circuit def circuits(self, backend: Optional[Backend] = None): """Create the circuit for the spectroscopy experiment. @@ -272,10 +306,10 @@ def circuits(self, backend: Optional[Backend] = None): Raises: QiskitError: - - If relative frequencies are used but no backend was given. + - If absolute frequencies are used but no backend is given. - If the backend configuration does not define dt. """ - # TODO this is temporarily logic. Need update of circuit data and processor logic. + # TODO this is temporary logic. Need update of circuit data and processor logic. self.set_analysis_options( data_processor=get_to_signal_processor( meas_level=self.run_options.meas_level, @@ -283,46 +317,30 @@ def circuits(self, backend: Optional[Backend] = None): ) ) - if not backend and not self._absolute: - raise QiskitError("Cannot run spectroscopy relative to qubit without a backend.") + if backend is None and self._absolute: + raise QiskitError("Cannot run spectroscopy absolute to qubit without a backend.") # Create a template circuit - freq_param = Parameter("frequency") - with pulse.build(backend=backend, name="spectroscopy") as sched: - pulse.set_frequency(freq_param, pulse.DriveChannel(self.physical_qubits[0])) - pulse.play( - pulse.GaussianSquare( - duration=self.experiment_options.duration, - amp=self.experiment_options.amp, - sigma=self.experiment_options.sigma, - width=self.experiment_options.width, - ), - pulse.DriveChannel(self.physical_qubits[0]), - ) - - gate = Gate(name="Spec", num_qubits=1, params=[freq_param]) - - circuit = QuantumCircuit(1) - circuit.append(gate, (0,)) - circuit.add_calibration(gate, (self.physical_qubits[0],), sched, params=[freq_param]) - circuit.measure_active() - - if not self._absolute: - center_freq = backend.defaults().qubit_freq_est[self.physical_qubits[0]] + sched, freq_param = self._spec_gate_schedule(backend) + circuit = self._template_circuit(freq_param) + circuit.add_calibration("Spec", (self.physical_qubits[0],), sched, params=[freq_param]) # Create the circuits to run circs = [] for freq in self._frequencies: - if not self._absolute: - freq += center_freq - freq = np.round(freq, decimals=3) + freq_shift = freq + if self._absolute: + center_freq = backend.defaults().qubit_freq_est[self.physical_qubits[0]] + freq_shift -= center_freq - assigned_circ = circuit.assign_parameters({freq_param: freq}, inplace=False) + freq_shift = np.round(freq_shift, decimals=3) + + assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) assigned_circ.metadata = { "experiment_type": self._type, "qubits": (self.physical_qubits[0],), - "xval": freq, + "xval": np.round(freq, decimals=3), "unit": "Hz", "amplitude": self.experiment_options.amp, "duration": self.experiment_options.duration, @@ -331,9 +349,6 @@ def circuits(self, backend: Optional[Backend] = None): "schedule": str(sched), } - if not self._absolute: - assigned_circ.metadata["center frequency"] = center_freq - try: assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt") except AttributeError as no_dt: diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index b832276cf1..349c55f134 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -65,7 +65,7 @@ def test_rabi_end_to_end(self): test_tol = 0.01 backend = RabiBackend() - rabi = Rabi(3) + rabi = Rabi(1) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) result = rabi.run(backend).analysis_result(0) @@ -74,7 +74,7 @@ def test_rabi_end_to_end(self): backend = RabiBackend(amplitude_to_angle=np.pi / 2) - rabi = Rabi(3) + rabi = Rabi(1) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) result = rabi.run(backend).analysis_result(0) self.assertEqual(result["quality"], "computer_good") @@ -82,7 +82,7 @@ def test_rabi_end_to_end(self): backend = RabiBackend(amplitude_to_angle=2.5 * np.pi) - rabi = Rabi(3) + rabi = Rabi(1) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 101)) result = rabi.run(backend).analysis_result(0) diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py index 40c3efde07..ee85c7db91 100644 --- a/test/calibration/test_update_library.py +++ b/test/calibration/test_update_library.py @@ -51,13 +51,14 @@ def test_amplitude(self): cals.add_schedule(xp) cals.add_schedule(x90p) - rabi = Rabi(3) + qubit = 1 + rabi = Rabi(qubit) rabi.set_experiment_options(amplitudes=np.linspace(-0.95, 0.95, 21)) exp_data = rabi.run(RabiBackend()) - for qubit in [0, 3]: + for qubit_ in [0, 1]: with self.assertRaises(CalibrationError): - cals.get_schedule("xp", qubits=qubit) + cals.get_schedule("xp", qubits=qubit_) to_update = [(np.pi, "amp", "xp"), (np.pi / 2, "amp", x90p)] @@ -75,34 +76,37 @@ def test_amplitude(self): rate = 2 * np.pi * result["popt"][1] amp = np.round(np.pi / rate, decimals=8) with pulse.build(name="xp") as expected: - pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(qubit)) - self.assertEqual(cals.get_schedule("xp", qubits=3), expected) + self.assertEqual(cals.get_schedule("xp", qubits=qubit), expected) amp = np.round(0.5 * np.pi / rate, decimals=8) with pulse.build(name="xp") as expected: - pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(3)) + pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(qubit)) - self.assertEqual(cals.get_schedule("x90p", qubits=3), expected) + self.assertEqual(cals.get_schedule("x90p", qubits=qubit), expected) def test_frequency(self): """Test calibrations update from spectroscopy.""" - backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) + qubit = 1 + peak_offset = 5.0e6 + backend = SpectroscopyBackend(line_width=2e6, freq_offset=peak_offset) + freq01 = backend.defaults().qubit_freq_est[qubit] + frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) / 1e6 - spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec = QubitSpectroscopy(qubit, frequencies, unit="MHz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) exp_data = spec.run(backend) result = exp_data.analysis_result(0) value = get_opt_value(result, "freq") - self.assertTrue(value < 5.1e6) - self.assertTrue(value > 4.9e6) + self.assertTrue(freq01 + peak_offset - 2e6 < value < freq01 + peak_offset + 2e6) self.assertEqual(result["quality"], "computer_good") # Test the integration with the BackendCalibrations cals = BackendCalibrations(FakeAthens()) - self.assertNotEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) + self.assertNotEqual(cals.get_qubit_frequencies()[qubit], result["popt"][2]) Frequency.update(cals, exp_data) - self.assertEqual(cals.get_qubit_frequencies()[3], result["popt"][2]) + self.assertEqual(cals.get_qubit_frequencies()[qubit], result["popt"][2]) diff --git a/test/mock_iq_backend.py b/test/mock_iq_backend.py index 4aaa49cf20..dcf77aa4bd 100644 --- a/test/mock_iq_backend.py +++ b/test/mock_iq_backend.py @@ -17,32 +17,16 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.providers.backend import BackendV1 as Backend -from qiskit.providers.models import QasmBackendConfiguration +from qiskit.test.mock import FakeOpenPulse2Q + from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options from .mock_job import MockJob -class MockIQBackend(Backend): +class MockIQBackend(FakeOpenPulse2Q): """An abstract backend for testing that can mock IQ data.""" - __configuration__ = { - "backend_name": "simulator", - "backend_version": "0", - "n_qubits": int(1), - "basis_gates": [], - "gates": [], - "local": True, - "simulator": True, - "conditional": False, - "open_pulse": False, - "memory": True, - "max_shots": int(1e6), - "coupling_map": [], - "dt": 0.1, - } - def __init__( self, iq_cluster_centers: Tuple[float, float, float, float] = (1.0, 1.0, -1.0, -1.0), @@ -53,10 +37,9 @@ def __init__( """ self._iq_cluster_centers = iq_cluster_centers self._iq_cluster_width = iq_cluster_width - self._rng = np.random.default_rng(0) - super().__init__(QasmBackendConfiguration(**self.__configuration__)) + super().__init__() def _default_options(self): """Default options of the test backend.""" diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 223f02c79f..7dbfb9e338 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -21,6 +21,7 @@ from qiskit.test import QiskitTestCase from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy +from qiskit_experiments.characterization.ef_spectroscopy import EFSpectroscopy from qiskit_experiments.analysis import get_opt_value @@ -36,7 +37,9 @@ def __init__( ): """Initialize the spectroscopy backend.""" - self.__configuration__["basis_gates"] = ["spec"] + super().__init__(iq_cluster_centers, iq_cluster_width) + + self.configuration().basis_gates = ["x"] self._linewidth = line_width self._freq_offset = freq_offset @@ -45,8 +48,8 @@ def __init__( def _compute_probability(self, circuit: QuantumCircuit) -> float: """Returns the probability based on the frequency.""" - set_freq = float(circuit.data[0][0].params[0]) - delta_freq = set_freq - self._freq_offset + freq_shift = next(iter(circuit.calibrations["Spec"]))[1][0] + delta_freq = freq_shift - self._freq_offset return np.exp(-(delta_freq ** 2) / (2 * self._linewidth ** 2)) @@ -57,55 +60,59 @@ def test_spectroscopy_end2end_classified(self): """End to end test of the spectroscopy experiment.""" backend = SpectroscopyBackend(line_width=2e6) + qubit = 1 + freq01 = backend.defaults().qubit_freq_est[qubit] + frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec = QubitSpectroscopy(qubit, frequencies, unit="Hz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) result = spec.run(backend).analysis_result(0) value = get_opt_value(result, "freq") - self.assertTrue(abs(value) < 1e6) + self.assertTrue(4.999e9 < value < 5.001e9) self.assertTrue(result["success"]) self.assertEqual(result["quality"], "computer_good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) - spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec = QubitSpectroscopy(qubit, frequencies, unit="Hz") spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) exp_data = spec.run(backend) result = exp_data.analysis_result(0) value = get_opt_value(result, "freq") - self.assertTrue(value < 5.1e6) - self.assertTrue(value > 4.9e6) + self.assertTrue(5.0049e9 < value < 5.0051e9) self.assertEqual(result["quality"], "computer_good") def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" backend = SpectroscopyBackend(line_width=2e6) + qubit = 0 + freq01 = backend.defaults().qubit_freq_est[qubit] + frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) / 1e6 - spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec = QubitSpectroscopy(qubit, frequencies, unit="MHz") result = spec.run(backend).analysis_result(0) value = get_opt_value(result, "freq") - self.assertTrue(abs(value) < 1e6) + self.assertTrue(freq01 - 2e6 < value < freq01 + 2e6) self.assertTrue(result["success"]) self.assertEqual(result["quality"], "computer_good") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) - spec = QubitSpectroscopy(3, np.linspace(-10.0, 10.0, 21), unit="MHz") + spec = QubitSpectroscopy(qubit, frequencies, unit="MHz") result = spec.run(backend).analysis_result(0) value = get_opt_value(result, "freq") - self.assertTrue(value < 5.1e6) - self.assertTrue(value > 4.9e6) + self.assertTrue(freq01 + 3e6 < value < freq01 + 8e6) self.assertEqual(result["quality"], "computer_good") spec.set_run_options(meas_return="avg") @@ -113,6 +120,30 @@ def test_spectroscopy_end2end_kerneled(self): value = get_opt_value(result, "freq") - self.assertTrue(value < 5.1e6) - self.assertTrue(value > 4.9e6) + self.assertTrue(freq01 + 3e6 < value < freq01 + 8e6) + self.assertEqual(result["quality"], "computer_good") + + def test_spectroscopy12_end2end_classified(self): + """End to end test of the spectroscopy experiment with an x pulse.""" + + backend = SpectroscopyBackend(line_width=2e6) + qubit = 0 + freq01 = backend.defaults().qubit_freq_est[qubit] + frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) + + # Note that the backend is not sophisticated enough to simulate an e-f + # transition so we run the test with g-e. + spec = EFSpectroscopy(qubit, frequencies, unit="Hz") + spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) + result = spec.run(backend).analysis_result(0) + + value = get_opt_value(result, "freq") + + self.assertTrue(freq01 - 2e6 < value < freq01 + 2e6) + self.assertTrue(result["success"]) self.assertEqual(result["quality"], "computer_good") + + # Test the circuits + circ = spec.circuits(backend)[0] + self.assertEqual(circ.data[0][0].name, "x") + self.assertEqual(circ.data[1][0].name, "Spec")