From 117fdb155fb9b2f4d910435eca684fc3bf95f18a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 17 Dec 2021 13:53:58 +0100 Subject: [PATCH 01/68] * First draft of new resonator spec. --- qiskit_experiments/library/__init__.py | 35 +++++---- .../library/characterization/__init__.py | 2 + .../resonator_spectroscopy_analysis.py | 0 .../characterization/qubit_spectroscopy.py | 6 +- .../resonator_spectroscopy.py | 72 +++++++++++++++++++ 5 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py create mode 100644 qiskit_experiments/library/characterization/resonator_spectroscopy.py diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 9dd3e5ac17..6b328fd2aa 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -71,6 +71,7 @@ ~characterization.RamseyXY ~characterization.FineFrequency ~characterization.ReadoutAngle + ~characterization.ResonatorSpectroscopy .. _calibration: @@ -103,21 +104,7 @@ class instance to manage parameters and pulse schedules. ~calibration.EFRoughXSXAmplitudeCal """ -from .calibration import ( - RoughDragCal, - FineDragCal, - FineXDragCal, - FineSXDragCal, - RoughAmplitudeCal, - RoughXSXAmplitudeCal, - EFRoughXSXAmplitudeCal, - FineAmplitudeCal, - FineXAmplitudeCal, - FineSXAmplitudeCal, - RoughFrequencyCal, - FrequencyCal, - FineFrequencyCal, -) + from .characterization import ( T1, T2Ramsey, @@ -138,7 +125,25 @@ class instance to manage parameters and pulse schedules. RamseyXY, FineFrequency, ReadoutAngle, + ResonatorSpectroscopy, ) + +from .calibration import ( + RoughDragCal, + FineDragCal, + FineXDragCal, + FineSXDragCal, + RoughAmplitudeCal, + RoughXSXAmplitudeCal, + EFRoughXSXAmplitudeCal, + FineAmplitudeCal, + FineXAmplitudeCal, + FineSXAmplitudeCal, + RoughFrequencyCal, + FrequencyCal, + FineFrequencyCal, +) + from .randomized_benchmarking import StandardRB, InterleavedRB from .tomography import StateTomography, ProcessTomography from .quantum_volume import QuantumVolume diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index b845af4d3a..38277634cb 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -41,6 +41,7 @@ FineDrag FineXDrag FineSXDrag + ResonatorSpectroscopy Analysis @@ -86,3 +87,4 @@ from .drag import RoughDrag from .readout_angle import ReadoutAngle from .fine_drag import FineDrag, FineXDrag, FineSXDrag +from .resonator_spectroscopy import ResonatorSpectroscopy diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index fd53dd2767..8744f5550f 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -167,12 +167,14 @@ def circuits(self): AttributeError: If backend to run on does not contain 'dt' configuration. """ if self.backend is None and self._absolute: - raise QiskitError("Cannot run spectroscopy absolute to qubit without a backend.") + raise QiskitError("Cannot run spectroscopy in absolute without a backend.") # Create a template circuit sched, freq_param = self._spec_gate_schedule(self.backend) circuit = self._template_circuit(freq_param) - circuit.add_calibration("Spec", (self.physical_qubits[0],), sched, params=[freq_param]) + circuit.add_calibration( + self.__spec_gate_name__, self.physical_qubits, sched, params=[freq_param] + ) # Get dt try: diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py new file mode 100644 index 0000000000..cc884d9e50 --- /dev/null +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -0,0 +1,72 @@ +# 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 experiment class for resonators.""" + +from typing import Optional, Iterable, Tuple + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter, Gate +from qiskit.providers import Backend +import qiskit.pulse as pulse +from qiskit.providers.options import Options +from qiskit.pulse import ScheduleBlock + +from qiskit_experiments.library.characterization import QubitSpectroscopy +from qiskit_experiments.curve_analysis import ResonanceAnalysis + + +class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): + """Class to analysis resonator spectroscopy.""" + + @classmethod + def _default_options(cls): + options = super()._default_options() + options.dimensionality_reduction="ToAbs" + return options + + +class ResonatorSpectroscopy(QubitSpectroscopy): + """Perform spectroscopy on the readout resonator.""" + + __spec_gate_name__ = "MSpec" + + 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,)) + + return circuit + + def _spec_gate_schedule( + self, backend: Optional[Backend] = None + ) -> Tuple[pulse.ScheduleBlock, Parameter]: + """Create the spectroscopy schedule.""" + + qubit = self.physical_qubits[0] + + freq_param = Parameter("frequency") + + with pulse.build(backend=backend, name="spectroscopy") as schedule: + pulse.shift_frequency(freq_param, pulse.MeasureChannel(qubit)) + 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.MeasureChannel(qubit), + ) + pulse.acquire(self.experiment_options.duration, qubit, pulse.MemorySlot(qubit)) + + return schedule, freq_param From b6821f581dba78ec7ebf903e2834fc9287da9ad0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 Jan 2022 15:15:12 +0100 Subject: [PATCH 02/68] * Resonator spectroscopy. --- .../characterization/qubit_spectroscopy.py | 23 +++++++++++++------ .../resonator_spectroscopy.py | 22 ++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 8744f5550f..db38043c6b 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -123,6 +123,21 @@ def __init__( self.analysis.set_options(ylabel="Signal [arb. unit]") + @property + def center_frequency(self) -> float: + """Returns the center frequency of the experiment. + + Returns: + The center frequency of the experiment. + + Raises: + QiskitError: If the experiment does not have a backend set. + """ + if self.backend is None: + raise QiskitError("backend not set. Cannot call center_frequency.") + + return self.backend.defaults().qubit_freq_est[self.physical_qubits[0]] + def _spec_gate_schedule( self, backend: Optional[Backend] = None ) -> Tuple[pulse.ScheduleBlock, Parameter]: @@ -182,18 +197,12 @@ def circuits(self): except AttributeError as no_dt: raise AttributeError("dt parameter is missing in backend configuration") from no_dt - # Get center frequency from backend - if self._absolute: - center_freq = self.backend.defaults().qubit_freq_est[self.physical_qubits[0]] - else: - center_freq = None - # Create the circuits to run circs = [] for freq in self._frequencies: freq_shift = freq if self._absolute: - freq_shift -= center_freq + freq_shift -= self.center_frequency freq_shift = np.round(freq_shift, decimals=3) assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index cc884d9e50..13caf2dd91 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -12,14 +12,13 @@ """Spectroscopy experiment class for resonators.""" -from typing import Optional, Iterable, Tuple +from typing import Optional, Tuple from qiskit import QuantumCircuit from qiskit.circuit import Parameter, Gate +from qiskit.exceptions import QiskitError from qiskit.providers import Backend import qiskit.pulse as pulse -from qiskit.providers.options import Options -from qiskit.pulse import ScheduleBlock from qiskit_experiments.library.characterization import QubitSpectroscopy from qiskit_experiments.curve_analysis import ResonanceAnalysis @@ -40,9 +39,24 @@ class ResonatorSpectroscopy(QubitSpectroscopy): __spec_gate_name__ = "MSpec" + @property + def center_frequency(self) -> float: + """Returns the center frequency of the experiment. + + Returns: + The center frequency of the experiment. + + Raises: + QiskitError: If the experiment does not have a backend set. + """ + if self.backend is None: + raise QiskitError("backend not set. Cannot call center_frequency.") + + return self.backend.defaults().meas_freq_est[self.physical_qubits[0]] + def _template_circuit(self, freq_param) -> QuantumCircuit: """Return the template quantum circuit.""" - circuit = QuantumCircuit(1) + circuit = QuantumCircuit(1, 1) circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) return circuit From 4b00b550b57183ae76737637521cc1a54a237743 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 Jan 2022 18:04:29 +0100 Subject: [PATCH 03/68] * Added IQ plotting. --- .../resonator_spectroscopy_analysis.py | 62 +++++++++++++++++++ .../resonator_spectroscopy.py | 40 ++++++++---- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index e69de29bb2..33d522b107 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -0,0 +1,62 @@ +# 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 analysis class for resonators.""" + +from typing import List, Tuple +import numpy as np + +from qiskit_experiments.curve_analysis import ResonanceAnalysis +from qiskit_experiments.framework import AnalysisResultData, ExperimentData +from qiskit_experiments.framework.matplotlib import get_non_gui_ax + +class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): + """Class to analysis resonator spectroscopy.""" + + @classmethod + def _default_options(cls): + options = super()._default_options() + options.dimensionality_reduction="ToAbs" + options.plot_iq_data = True + return options + + def _run_analysis( + self, experiment_data: ExperimentData + ) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]: + """Wrap the analysis to optionally plot the IQ data.""" + analysis_results, figures = super()._run_analysis(experiment_data) + + if self.options.plot_iq_data: + axis = get_non_gui_ax() + figure = axis.get_figure() + figure.set_size_inches(*self.options.style.figsize) + + iqs = [] + + for datum in experiment_data.data(): + if "memory" in datum: + mem = np.array(datum["memory"]) + + # Average single-shot data. + if len(mem.shape) == 3: + iqs.append(np.average(mem.reshape(mem.shape[0], mem.shape[2]), axis=0)) + + iqs = np.array(iqs) + axis.scatter(iqs[:, 0], iqs[:, 1], color="b") + axis.set_xlabel("In phase [arb. units]", fontsize=self.options.style.axis_label_size) + axis.set_ylabel("Quadrature [arb. units]", fontsize=self.options.style.axis_label_size) + axis.tick_params(labelsize=self.options.style.tick_label_size) + axis.grid(True) + + figures.append(figure) + + return analysis_results, figures diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 13caf2dd91..cfdaa64837 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -12,7 +12,7 @@ """Spectroscopy experiment class for resonators.""" -from typing import Optional, Tuple +from typing import Iterable, Optional, Tuple from qiskit import QuantumCircuit from qiskit.circuit import Parameter, Gate @@ -21,17 +21,7 @@ import qiskit.pulse as pulse from qiskit_experiments.library.characterization import QubitSpectroscopy -from qiskit_experiments.curve_analysis import ResonanceAnalysis - - -class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): - """Class to analysis resonator spectroscopy.""" - - @classmethod - def _default_options(cls): - options = super()._default_options() - options.dimensionality_reduction="ToAbs" - return options +from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis class ResonatorSpectroscopy(QubitSpectroscopy): @@ -39,6 +29,32 @@ class ResonatorSpectroscopy(QubitSpectroscopy): __spec_gate_name__ = "MSpec" + def __init__( + self, + qubit: int, + frequencies: Iterable[float], + backend: Optional[Backend] = None, + absolute: bool = True, + ): + """ + A spectroscopy experiment run by setting the frequency of the readout drive. + The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. + The spectroscopy pulse has the following parameters: + - amp: The amplitude of the pulse must be between 0 and 1, the default is 0.1. + - duration: The duration of the spectroscopy pulse in samples, the default is 1000 samples. + - sigma: The standard deviation of the pulse, the default is duration / 4. + - width: The width of the flat-top in the pulse, the default is 0, i.e. a Gaussian. + + Args: + qubit: The qubit on which to run readout spectroscopy. + frequencies: The frequencies to scan in the experiment, in Hz. + backend: Optional, the backend to run the experiment on. + absolute: Boolean to specify if the frequencies are absolute or relative to the + qubit frequency in the backend. + """ + super().__init__(qubit, frequencies, backend, absolute) + self.analysis = ResonatorSpectroscopyAnalysis() + @property def center_frequency(self) -> float: """Returns the center frequency of the experiment. From f568738c52e6b1422a6ba5431ffe3a988139bbef Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 Jan 2022 14:21:26 +0100 Subject: [PATCH 04/68] * Adding methodology to handle the Classical bit. --- .../analysis/resonator_spectroscopy_analysis.py | 2 ++ .../library/characterization/resonator_spectroscopy.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 33d522b107..149a1d803c 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -15,6 +15,7 @@ from typing import List, Tuple import numpy as np +import qiskit_experiments.curve_analysis as curve from qiskit_experiments.curve_analysis import ResonanceAnalysis from qiskit_experiments.framework import AnalysisResultData, ExperimentData from qiskit_experiments.framework.matplotlib import get_non_gui_ax @@ -26,6 +27,7 @@ class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): def _default_options(cls): options = super()._default_options() options.dimensionality_reduction="ToAbs" + options.result_parameters = [curve.ParameterRepr("freq", "meas_freq", "Hz")] options.plot_iq_data = True return options diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index cfdaa64837..c8809e5e3b 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -72,7 +72,12 @@ def center_frequency(self) -> float: def _template_circuit(self, freq_param) -> QuantumCircuit: """Return the template quantum circuit.""" - circuit = QuantumCircuit(1, 1) + if self.backend is not None: + cbits = self.backend.configuration().num_qubits + else: + cbits = 1 + + circuit = QuantumCircuit(1, cbits) circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) return circuit From 660766ae1a2c248d7e6f14f9bf3b6f5cbec51da0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 Jan 2022 14:59:00 +0100 Subject: [PATCH 05/68] * Switched to a parallelizable methodology for the classical bit. --- .../characterization/qubit_spectroscopy.py | 38 ++++++------- .../resonator_spectroscopy.py | 54 +++++++++++++++---- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index db38043c6b..803a528ee4 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -166,6 +166,20 @@ def _template_circuit(self, freq_param) -> QuantumCircuit: return circuit + def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): + """Helper method to add the metadata to avoid code duplication with subclasses.""" + circuit.metadata = { + "experiment_type": self._type, + "qubits": (self.physical_qubits[0],), + "xval": np.round(freq, decimals=3), + "unit": "Hz", + "amplitude": self.experiment_options.amp, + "duration": self.experiment_options.duration, + "sigma": self.experiment_options.sigma, + "width": self.experiment_options.width, + "schedule": str(sched), + } + def circuits(self): """Create the circuit for the spectroscopy experiment. @@ -176,9 +190,8 @@ def circuits(self): circuits: The circuits that will run the spectroscopy experiment. Raises: - QiskitError: - - If absolute frequencies are used but no backend is given. - - If the backend configuration does not define dt. + QiskitError: If absolute frequencies are used but no backend is given. + QiskitError: If the backend configuration does not define dt. AttributeError: If backend to run on does not contain 'dt' configuration. """ if self.backend is None and self._absolute: @@ -191,12 +204,6 @@ def circuits(self): self.__spec_gate_name__, self.physical_qubits, sched, params=[freq_param] ) - # Get dt - try: - dt_factor = getattr(self.backend.configuration(), "dt") - except AttributeError as no_dt: - raise AttributeError("dt parameter is missing in backend configuration") from no_dt - # Create the circuits to run circs = [] for freq in self._frequencies: @@ -206,18 +213,7 @@ def circuits(self): 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": np.round(freq, decimals=3), - "unit": "Hz", - "amplitude": self.experiment_options.amp, - "duration": self.experiment_options.duration, - "sigma": self.experiment_options.sigma, - "width": self.experiment_options.width, - "schedule": str(sched), - "dt": dt_factor, - } + self._add_metadata(assigned_circ, freq, sched) circs.append(assigned_circ) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index c8809e5e3b..fa7aab04c0 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -13,9 +13,10 @@ """Spectroscopy experiment class for resonators.""" from typing import Iterable, Optional, Tuple +import numpy as np from qiskit import QuantumCircuit -from qiskit.circuit import Parameter, Gate +from qiskit.circuit import Parameter from qiskit.exceptions import QiskitError from qiskit.providers import Backend import qiskit.pulse as pulse @@ -27,8 +28,6 @@ class ResonatorSpectroscopy(QubitSpectroscopy): """Perform spectroscopy on the readout resonator.""" - __spec_gate_name__ = "MSpec" - def __init__( self, qubit: int, @@ -70,15 +69,10 @@ def center_frequency(self) -> float: return self.backend.defaults().meas_freq_est[self.physical_qubits[0]] - def _template_circuit(self, freq_param) -> QuantumCircuit: + def _meas_template_circuit(self) -> QuantumCircuit: """Return the template quantum circuit.""" - if self.backend is not None: - cbits = self.backend.configuration().num_qubits - else: - cbits = 1 - - circuit = QuantumCircuit(1, cbits) - circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) + circuit = QuantumCircuit(1, 1) + circuit.measure(0, 0) return circuit @@ -105,3 +99,41 @@ def _spec_gate_schedule( pulse.acquire(self.experiment_options.duration, qubit, pulse.MemorySlot(qubit)) return schedule, freq_param + + def circuits(self): + """Create the circuit for the spectroscopy experiment. + + The circuits are based on a GaussianSquare pulse and a frequency_shift instruction + encapsulated in a gate. + + Returns: + circuits: The circuits that will run the spectroscopy experiment. + + Raises: + QiskitError: If absolute frequencies are used but no backend is given. + QiskitError: If the backend configuration does not define dt. + AttributeError: If backend to run on does not contain 'dt' configuration. + """ + if self.backend is None and self._absolute: + raise QiskitError("Cannot run spectroscopy in absolute without a backend.") + + # Create a template circuit + sched, freq_param = self._spec_gate_schedule(self.backend) + + # Create the circuits to run + circs = [] + for freq in self._frequencies: + freq_shift = freq + if self._absolute: + freq_shift -= self.center_frequency + freq_shift = np.round(freq_shift, decimals=3) + + sched_ = sched.assign_parameters({freq_param: freq_shift}, inplace=False) + + circuit = self._meas_template_circuit() + circuit.add_calibration("measure", self.physical_qubits, sched_) + self._add_metadata(circuit, freq, sched) + + circs.append(circuit) + + return circs From c36d3299f6e612133f09786d913d6fc9b3cb388f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Jan 2022 17:54:44 +0100 Subject: [PATCH 06/68] * Data processing for the resonator spectroscopy experiment. The resonator spectroscopy experiment requires special data processing since the IQ points rotate around in the IQ plane as the frequency of the measurement pulse is shifted. --- qiskit_experiments/data_processing/nodes.py | 20 +++++++++ .../data_processing/processor_library.py | 26 +++++++++++- .../resonator_spectroscopy.py | 41 ++++++++++++++++++- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 7fb6f7e991..219824e3a4 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -374,6 +374,26 @@ def _process(self, data: np.ndarray) -> np.ndarray: return data[..., 1] * self.scale +class ToAbs(IQPart): + """IQ data post-processing. Take the absolute value of the IQ point.""" + + def _process(self, data: np.array) -> np.array: + """Take the absolute value of the IQ data. + + Args: + data: An N-dimensional array of complex IQ point as [real, imaginary]. + + Returns: + A N-1 dimensional array, each entry is the absolute value of the given IQ data. + """ + + # There seems to be an issue with the uncertainties package since + # np.sqrt(ufloat(4, float("nan"))) does not work but + # np.power(ufloat(4, float("nan")), 0.5) does work. + + return np.power(data[..., 0] ** 2 + data[..., 1] ** 2, 0.5) * self.scale + + class Probability(DataAction): r"""Compute the mean probability of a single measurement outcome from counts. diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index 8ced9d57af..679f891432 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -12,17 +12,27 @@ """A collection of functions that return various data processors.""" +from enum import Enum + from qiskit.qobj.utils import MeasLevel from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing import nodes +class ProjectorType(str, Enum): + """Types of projectors for data dimensionality reduction.""" + SVD = "SVD" + ABS = "ABS" + REAL = "REAL" + IMAG = "IMAG" + def get_processor( meas_level: MeasLevel = MeasLevel.CLASSIFIED, meas_return: str = "avg", normalize: bool = True, + dimensionality_reduction: ProjectorType = ProjectorType.SVD, ) -> DataProcessor: """Get a DataProcessor that produces a continuous signal given the options. @@ -30,6 +40,8 @@ def get_processor( meas_level: The measurement level of the data to process. meas_return: The measurement return (single or avg) of the data to process. normalize: Add a data normalization node to the Kerneled data processor. + dimensionality_reduction: A string to represent the dimensionality reduction + node. Must be one of SVD, ABS, REAL, IMAG. Returns: An instance of DataProcessor capable of dealing with the given options. @@ -37,14 +49,24 @@ def get_processor( Raises: DataProcessorError: if the measurement level is not supported. """ + projectors = { + "SVD": nodes.SVD, + "ABS": nodes.ToAbs, + "REAL": nodes.ToReal, + "IMAG": nodes.ToImag, + } + if meas_level == MeasLevel.CLASSIFIED: return DataProcessor("counts", [nodes.Probability("1")]) if meas_level == MeasLevel.KERNELED: + + projector = projectors[dimensionality_reduction] + if meas_return == "single": - processor = DataProcessor("memory", [nodes.AverageData(axis=1), nodes.SVD()]) + processor = DataProcessor("memory", [nodes.AverageData(axis=1), projector()]) else: - processor = DataProcessor("memory", [nodes.SVD()]) + processor = DataProcessor("memory", [projector()]) if normalize: processor.append(nodes.MinMaxNormalize()) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index fa7aab04c0..15e81287dd 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -23,10 +23,31 @@ from qiskit_experiments.library.characterization import QubitSpectroscopy from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis +from qiskit_experiments.data_processing.processor_library import get_processor, ProjectorType class ResonatorSpectroscopy(QubitSpectroscopy): - """Perform spectroscopy on the readout resonator.""" + """Perform spectroscopy on the readout resonator. + + # section: overview + This experiment does spectroscopy on the readout resonator. It applies the following + circuit + + .. parsed-literal:: + + ┌─┐ + q: ┤M├ + └╥┘ + c: 1/═╩═ + 0 + + where a spectroscopy pulse is attached to the measurement instruction. When doing + readout resonator spectroscopy, each measured IQ point has a frequency dependent + phase. Close to the resonance, the IQ points start rotating around in the IQ plan. + To create a meaningful signal this experiment therefore uses a custom data processor + where the dimensionality reducing SVD is replaced by the absolute value of the IQ + point. + """ def __init__( self, @@ -53,6 +74,22 @@ def __init__( """ super().__init__(qubit, frequencies, backend, absolute) self.analysis = ResonatorSpectroscopyAnalysis() + self._set_analysis_data_processor() + + def _set_analysis_data_processor(self): + """Keep the data processor consistent with the run options.""" + processor = get_processor( + self.run_options.meas_level, + self.run_options.meas_return, + self.analysis.options.normalization, + dimensionality_reduction=ProjectorType.ABS, + ) + self.analysis.set_options(data_processor=processor) + + def set_run_options(self, **fields): + """Wrap set run options to keep the data processor consistent.""" + super().set_run_options(**fields) + self._set_analysis_data_processor() @property def center_frequency(self) -> float: @@ -96,7 +133,7 @@ def _spec_gate_schedule( ), pulse.MeasureChannel(qubit), ) - pulse.acquire(self.experiment_options.duration, qubit, pulse.MemorySlot(qubit)) + pulse.acquire(self.experiment_options.duration, qubit, pulse.MemorySlot(0)) return schedule, freq_param From 3db95eee3229e9243c174f56568654919998aecc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Jan 2022 18:26:42 +0100 Subject: [PATCH 07/68] * Added a test for the ToAbs node. --- test/data_processing/test_nodes.py | 39 +++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 2fb520c188..c3ed7e71d2 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -15,10 +15,11 @@ from test.base import QiskitExperimentsTestCase import numpy as np -from uncertainties import unumpy as unp +from uncertainties import unumpy as unp, ufloat from qiskit_experiments.data_processing.nodes import ( SVD, + ToAbs, AverageData, MinMaxNormalize, Probability, @@ -137,6 +138,42 @@ def test_iq_averaging(self): ) +class TestToAbs(QiskitExperimentsTestCase): + """Test the ToAbs node.""" + + def test_simple(self): + """Simple test to check the it runs.""" + + data = [ + [[ufloat(2.0, np.nan), ufloat(2.0, np.nan)]], + [[ufloat(1.0, np.nan), ufloat(2.0, np.nan)]], + [[ufloat(2.0, 0.2), ufloat(3.0, 0.3)]], + ] + + processed = ToAbs()(np.array(data)) + + val = np.sqrt(2**2 + 3**2) + val_err = np.sqrt(2**2*0.2**2+2**2*0.3**2) / val + + expected = np.array([ + [ufloat(np.sqrt(8), np.nan)], + [ufloat(np.sqrt(5), np.nan)], + [ufloat(val, val_err)], + ]) + + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + unp.nominal_values(expected), + decimal=-8, + ) + + np.testing.assert_array_almost_equal( + unp.std_devs(processed), + unp.std_devs(expected), + decimal=-8, + ) + + class TestNormalize(QiskitExperimentsTestCase): """Test the normalization node.""" From e1de77f2888e14b8548b8616a47360d5911e925c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 17 Jan 2022 18:45:08 +0100 Subject: [PATCH 08/68] * Black and tests for resonator spectroscopy. --- .../data_processing/processor_library.py | 2 + .../resonator_spectroscopy_analysis.py | 5 +- qiskit_experiments/test/mock_iq_backend.py | 17 +++- test/data_processing/test_nodes.py | 18 ++-- test/test_resonator_spectroscopy.py | 94 +++++++++++++++++++ 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 test/test_resonator_spectroscopy.py diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index 679f891432..6fa3236281 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -20,8 +20,10 @@ from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing import nodes + class ProjectorType(str, Enum): """Types of projectors for data dimensionality reduction.""" + SVD = "SVD" ABS = "ABS" REAL = "REAL" diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 149a1d803c..e21026520d 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -20,19 +20,20 @@ from qiskit_experiments.framework import AnalysisResultData, ExperimentData from qiskit_experiments.framework.matplotlib import get_non_gui_ax + class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): """Class to analysis resonator spectroscopy.""" @classmethod def _default_options(cls): options = super()._default_options() - options.dimensionality_reduction="ToAbs" + options.dimensionality_reduction = "ToAbs" options.result_parameters = [curve.ParameterRepr("freq", "meas_freq", "Hz")] options.plot_iq_data = True return options def _run_analysis( - self, experiment_data: ExperimentData + self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]: """Wrap the analysis to optionally plot the IQ data.""" analysis_results, figures = super()._run_analysis(experiment_data) diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index e8b561a471..2194a21caf 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -52,7 +52,7 @@ def _default_options(self): meas_return="single", ) - def _draw_iq_shots(self, prob, shots) -> List[List[List[float]]]: + def _draw_iq_shots(self, prob, shots, phase: float = 0.0) -> List[List[List[float]]]: """Produce an IQ shot.""" rand_i = self._rng.normal(0, self._iq_cluster_width, size=shots) @@ -68,6 +68,10 @@ def _draw_iq_shots(self, prob, shots) -> List[List[List[float]]]: point_i = self._iq_cluster_centers[2] + rand_i[idx] point_q = self._iq_cluster_centers[3] + rand_q[idx] + if not np.allclose(phase, 0.0): + complex_iq = (point_i + 1.0j * point_q) * np.exp(1.0j * phase) + point_i, point_q = np.real(complex_iq), np.imag(complex_iq) + memory.append([[point_i, point_q]]) return memory @@ -86,6 +90,14 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: The probability that the binaomial distribution will use to generate an IQ shot. """ + def _iq_phase(self, circuit: QuantumCircuit) -> float: + """Sub-classes can override this method to introduce a phase in the IQ plan. + + This is needed, to test the resonator spectroscopy where the point in the IQ + plan has a frequency-dependent phase rotation. + """ + return 0.0 + def run(self, run_input, **options): """Run the IQ backend.""" @@ -117,7 +129,8 @@ def run(self, run_input, **options): ones = np.sum(self._rng.binomial(1, prob, size=shots)) run_result["data"] = {"counts": {"1": ones, "0": shots - ones}} else: - memory = self._draw_iq_shots(prob, shots) + phase = self._iq_phase(circ) + memory = self._draw_iq_shots(prob, shots, phase) if meas_return == "avg": memory = np.average(np.array(memory), axis=0).tolist() diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index c3ed7e71d2..237e0bd768 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -152,14 +152,16 @@ def test_simple(self): processed = ToAbs()(np.array(data)) - val = np.sqrt(2**2 + 3**2) - val_err = np.sqrt(2**2*0.2**2+2**2*0.3**2) / val - - expected = np.array([ - [ufloat(np.sqrt(8), np.nan)], - [ufloat(np.sqrt(5), np.nan)], - [ufloat(val, val_err)], - ]) + val = np.sqrt(2 ** 2 + 3 ** 2) + val_err = np.sqrt(2 ** 2 * 0.2 ** 2 + 2 ** 2 * 0.3 ** 2) / val + + expected = np.array( + [ + [ufloat(np.sqrt(8), np.nan)], + [ufloat(np.sqrt(5), np.nan)], + [ufloat(val, val_err)], + ] + ) np.testing.assert_array_almost_equal( unp.nominal_values(processed), diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py new file mode 100644 index 0000000000..a87ff7b50a --- /dev/null +++ b/test/test_resonator_spectroscopy.py @@ -0,0 +1,94 @@ +# 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 tests for resonator spectroscop experiment.""" + +from test.base import QiskitExperimentsTestCase +from typing import Tuple +import numpy as np + +from qiskit import QuantumCircuit + +from qiskit_experiments.library import ResonatorSpectroscopy +from qiskit_experiments.test.mock_iq_backend import MockIQBackend + + +class ResonatorSpectroscopyBackend(MockIQBackend): + """A simple and primitive backend to test spectroscopy experiments.""" + + def __init__( + self, + line_width: float = 2e6, + freq_offset: float = 0.0, + iq_cluster_centers: Tuple[float, float, float, float] = (-1.0, 0.0, 0.0, 0.0), + iq_cluster_width: float = 0.2, + ): + """Initialize the spectroscopy backend.""" + + super().__init__(iq_cluster_centers, iq_cluster_width) + + self._linewidth = line_width + self._freq_offset = freq_offset + + super().__init__(iq_cluster_centers, iq_cluster_width) + + def _compute_probability(self, circuit: QuantumCircuit) -> float: + """Returns the probability based on the frequency.""" + freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency + delta_freq = freq_shift - self._freq_offset + + return np.exp(-(delta_freq ** 2) / (2 * self._linewidth ** 2)) + + def _iq_phase(self, circuit: QuantumCircuit) -> float: + """Add a phase to the IQ point depending on how far we are from the resonance. + + This will cause the IQ points to rotate around in the IQ plane when we approach the + resonance which introduces and extra complication that the data processor needs to + properly handle. + """ + freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency + delta_freq = freq_shift - self._freq_offset + + return delta_freq / self._linewidth + + +class TestResonatorSpectroscopy(QiskitExperimentsTestCase): + """Tests for the resonator spectroscopy experiment.""" + + def test_end_to_end(self): + """Test the experiment from end to end.""" + + qubit = 1 + backend = ResonatorSpectroscopyBackend(freq_offset=10e6) + res_freq = backend.defaults().meas_freq_est[qubit] + + frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 51) + spec = ResonatorSpectroscopy(qubit, frequencies=frequencies, backend=backend) + + expdata = spec.run(backend) + result = expdata.analysis_results(1) + value = result.value.value + + self.assertTrue(6.60999e9 < value < 6.61001e9) + self.assertEqual(result.quality, "good") + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = ResonatorSpectroscopy(1, np.linspace(100, 150, 20) * 1e6) + loaded_exp = ResonatorSpectroscopy.from_config(exp.config()) + self.assertNotEqual(exp, loaded_exp) + self.assertTrue(self.experiments_equiv(exp, loaded_exp)) + + def test_roundtrip_serializable(self): + """Test round trip JSON serialization""" + exp = ResonatorSpectroscopy(1, np.linspace(int(100e6), int(150e6), int(20e6))) + self.assertRoundTripSerializable(exp, self.experiments_equiv) From a807b1022a0c6d58abd60ff6a20b9befc5239468 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 17 Jan 2022 19:00:59 +0100 Subject: [PATCH 09/68] * Reno and lint. --- .../library/characterization/resonator_spectroscopy.py | 2 +- qiskit_experiments/test/mock_iq_backend.py | 1 + .../notes/resonator-spectroscopy-89f790412838ba5b.yaml | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 15e81287dd..07d90c6d63 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -22,8 +22,8 @@ import qiskit.pulse as pulse from qiskit_experiments.library.characterization import QubitSpectroscopy -from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis from qiskit_experiments.data_processing.processor_library import get_processor, ProjectorType +from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis class ResonatorSpectroscopy(QubitSpectroscopy): diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index 2194a21caf..6d471ada5d 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -90,6 +90,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: The probability that the binaomial distribution will use to generate an IQ shot. """ + # pylint: disable=unused-argument def _iq_phase(self, circuit: QuantumCircuit) -> float: """Sub-classes can override this method to introduce a phase in the IQ plan. diff --git a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml new file mode 100644 index 0000000000..15021e4e02 --- /dev/null +++ b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + This change introduces a new experiment that allows users to run spectroscopy + on readout resonators. This is done by attaching a custom pulse-schedule to + the measure instruction. Furthermore, a new data processing node is introduced + that takes the absolute value of the IQ points. This node is needed to analyse + readout resonator spectroscopy IQ data since it rotates around in the IQ plane. From d3674a16d23931fbe14b8507d6af42119c94bcee Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 17 Jan 2022 19:02:19 +0100 Subject: [PATCH 10/68] * Docs. --- .../library/characterization/resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 07d90c6d63..1fe8d5bfe0 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -70,7 +70,7 @@ def __init__( frequencies: The frequencies to scan in the experiment, in Hz. backend: Optional, the backend to run the experiment on. absolute: Boolean to specify if the frequencies are absolute or relative to the - qubit frequency in the backend. + resonator frequency in the backend. """ super().__init__(qubit, frequencies, backend, absolute) self.analysis = ResonatorSpectroscopyAnalysis() From 7f920fda12981e425c93afefa0ee568122fe16e1 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 19 Jan 2022 15:46:39 +0100 Subject: [PATCH 11/68] * Change the sqrt in ToAbs node to the unp version. --- qiskit_experiments/data_processing/nodes.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 219824e3a4..60c2a7b548 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -386,12 +386,7 @@ def _process(self, data: np.array) -> np.array: Returns: A N-1 dimensional array, each entry is the absolute value of the given IQ data. """ - - # There seems to be an issue with the uncertainties package since - # np.sqrt(ufloat(4, float("nan"))) does not work but - # np.power(ufloat(4, float("nan")), 0.5) does work. - - return np.power(data[..., 0] ** 2 + data[..., 1] ** 2, 0.5) * self.scale + return unp.sqrt(data[..., 0] ** 2 + data[..., 1] ** 2) * self.scale class Probability(DataAction): From 7331ae024036881b8d4465be5b3c31c28acba005 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:03:00 +0100 Subject: [PATCH 12/68] * Refactored processor library. --- .../data_processing/processor_library.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index 6fa3236281..51b040803c 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -13,6 +13,7 @@ """A collection of functions that return various data processors.""" from enum import Enum +from typing import Union from qiskit.qobj.utils import MeasLevel @@ -21,20 +22,19 @@ from qiskit_experiments.data_processing import nodes -class ProjectorType(str, Enum): +class ProjectorType(Enum): """Types of projectors for data dimensionality reduction.""" - - SVD = "SVD" - ABS = "ABS" - REAL = "REAL" - IMAG = "IMAG" + SVD = nodes.SVD + ABS = nodes.ToAbs + REAL = nodes.ToReal + IMAG = nodes.ToImag def get_processor( meas_level: MeasLevel = MeasLevel.CLASSIFIED, meas_return: str = "avg", normalize: bool = True, - dimensionality_reduction: ProjectorType = ProjectorType.SVD, + dimensionality_reduction: Union[str, ProjectorType] = ProjectorType.SVD, ) -> DataProcessor: """Get a DataProcessor that produces a continuous signal given the options. @@ -50,20 +50,26 @@ def get_processor( Raises: DataProcessorError: if the measurement level is not supported. + DataProcessorError: if the wrong dimensionality reduction for kerneled data + is specified. """ - projectors = { - "SVD": nodes.SVD, - "ABS": nodes.ToAbs, - "REAL": nodes.ToReal, - "IMAG": nodes.ToImag, - } - if meas_level == MeasLevel.CLASSIFIED: return DataProcessor("counts", [nodes.Probability("1")]) if meas_level == MeasLevel.KERNELED: - projector = projectors[dimensionality_reduction] + try: + if isinstance(dimensionality_reduction, ProjectorType): + projector_name = dimensionality_reduction.name + else: + projector_name = dimensionality_reduction + + projector = ProjectorType[projector_name].value + + except KeyError as error: + raise DataProcessorError( + f"Invalid dimensionality reduction: {dimensionality_reduction}." + ) from error if meas_return == "single": processor = DataProcessor("memory", [nodes.AverageData(axis=1), projector()]) From 2c1c124cf0b209b21e37511383f4b946252bcbd9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:20:36 +0100 Subject: [PATCH 13/68] * processor_library docstring. --- qiskit_experiments/data_processing/processor_library.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index 51b040803c..a6b114d059 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -42,8 +42,12 @@ def get_processor( meas_level: The measurement level of the data to process. meas_return: The measurement return (single or avg) of the data to process. normalize: Add a data normalization node to the Kerneled data processor. - dimensionality_reduction: A string to represent the dimensionality reduction - node. Must be one of SVD, ABS, REAL, IMAG. + dimensionality_reduction: An optional string or instance of :class:`ProjectorType` + to represent the dimensionality reduction node for Kerneled data. For the + supported nodes, see :class:`ProjectorType`. Typically, these nodes convert + complex IQ data to real data, for example by performing a singular-value + decomposition. This argument is only needed for Kerneled data (i.e. level 1) + and can thus be ignored if Classified data (the default) is used. Returns: An instance of DataProcessor capable of dealing with the given options. From fb3eaf15b6331f644809e5603aeb39ca793387a6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:29:45 +0100 Subject: [PATCH 14/68] * Add ResSpecAnalysis to tree. * Docs and refs. --- qiskit_experiments/library/characterization/__init__.py | 2 ++ .../library/characterization/analysis/__init__.py | 1 + .../library/characterization/resonator_spectroscopy.py | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index 38277634cb..86621342fa 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -60,6 +60,7 @@ FineAmplitudeAnalysis RamseyXYAnalysis ReadoutAngleAnalysis + ResonatorSpectroscopyAnalysis """ from .analysis import ( @@ -72,6 +73,7 @@ T1Analysis, CrossResonanceHamiltonianAnalysis, ReadoutAngleAnalysis, + ResonatorSpectroscopyAnalysis, ) from .t1 import T1 diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index e100871223..79eb8106c8 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -22,3 +22,4 @@ from .t1_analysis import T1Analysis from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis from .readout_angle_analysis import ReadoutAngleAnalysis +from .resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 1fe8d5bfe0..a0050c3e9e 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -47,6 +47,12 @@ class ResonatorSpectroscopy(QubitSpectroscopy): To create a meaningful signal this experiment therefore uses a custom data processor where the dimensionality reducing SVD is replaced by the absolute value of the IQ point. + + # section: analysis_ref + :py:class:`ResonatorSpectroscopyAnalysis` + + # section: see_also + qiskit_experiments.library.characterization.qubit_spectroscopy.QubitSpectroscopy """ def __init__( From a0072d8be7827341dc5c914c84066c94c79039c7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:31:16 +0100 Subject: [PATCH 15/68] Update qiskit_experiments/library/characterization/resonator_spectroscopy.py Co-authored-by: Naoki Kanazawa --- .../library/characterization/resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index a0050c3e9e..f5f2f223ad 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -43,7 +43,7 @@ class ResonatorSpectroscopy(QubitSpectroscopy): where a spectroscopy pulse is attached to the measurement instruction. When doing readout resonator spectroscopy, each measured IQ point has a frequency dependent - phase. Close to the resonance, the IQ points start rotating around in the IQ plan. + phase. Close to the resonance, the IQ points start rotating around in the IQ plane. To create a meaningful signal this experiment therefore uses a custom data processor where the dimensionality reducing SVD is replaced by the absolute value of the IQ point. From 2355825f8f8b48bd9e9ef337a4a30ee57928a077 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:33:04 +0100 Subject: [PATCH 16/68] * Simplified resonator spectroscopy doc. --- .../library/characterization/resonator_spectroscopy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index a0050c3e9e..d42d5a7274 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -44,10 +44,9 @@ class ResonatorSpectroscopy(QubitSpectroscopy): where a spectroscopy pulse is attached to the measurement instruction. When doing readout resonator spectroscopy, each measured IQ point has a frequency dependent phase. Close to the resonance, the IQ points start rotating around in the IQ plan. - To create a meaningful signal this experiment therefore uses a custom data processor - where the dimensionality reducing SVD is replaced by the absolute value of the IQ - point. - + This effect must be accounted for in the data processing to produce a meaningful + signal. + # section: analysis_ref :py:class:`ResonatorSpectroscopyAnalysis` From 5515f2d10bde2c4ece06a0a0fa5142802be99198 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:42:28 +0100 Subject: [PATCH 17/68] * Added key word arguments to init for one liner execution. --- .../library/characterization/resonator_spectroscopy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 343a841274..6003c3b538 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -60,6 +60,7 @@ def __init__( frequencies: Iterable[float], backend: Optional[Backend] = None, absolute: bool = True, + **experiment_options, ): """ A spectroscopy experiment run by setting the frequency of the readout drive. @@ -76,11 +77,14 @@ def __init__( backend: Optional, the backend to run the experiment on. absolute: Boolean to specify if the frequencies are absolute or relative to the resonator frequency in the backend. + experiment_options: Key word arguments used to set the experiment options. """ super().__init__(qubit, frequencies, backend, absolute) self.analysis = ResonatorSpectroscopyAnalysis() self._set_analysis_data_processor() + self.set_experiment_options(**experiment_options) + def _set_analysis_data_processor(self): """Keep the data processor consistent with the run options.""" processor = get_processor( From 5a07e94552cddc005beadb1d0d488e143a7efc4d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 14:50:58 +0100 Subject: [PATCH 18/68] * Release note text. --- .../resonator-spectroscopy-89f790412838ba5b.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml index 15021e4e02..70dcbd0b4f 100644 --- a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml +++ b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml @@ -1,8 +1,12 @@ --- features: - | - This change introduces a new experiment that allows users to run spectroscopy + This change introduces the new experiment + :py:class:`~qiskit_experiments.library.ResonatorSpectroscopy` to run spectroscopy on readout resonators. This is done by attaching a custom pulse-schedule to - the measure instruction. Furthermore, a new data processing node is introduced - that takes the absolute value of the IQ points. This node is needed to analyse - readout resonator spectroscopy IQ data since it rotates around in the IQ plane. + the measure instruction. Furthermore, a new data processing node + :py:class`~qiskit_experiments.data_processing.nodes.ToAbs` is introduced to + take the absolute value of the IQ points. This node is needed to analyse readout + resonator spectroscopy IQ data since it rotates around in the IQ plane. Note that + the resonator spectroscopy experiment may cause errors on backends that do not + support circuit instructions with measurement schedules attached to them. From bde77fbe8cae0f3648c6bd6717e84644f33f2e99 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 16:28:00 +0100 Subject: [PATCH 19/68] * Create the Spectroscopy base class for spectroscopy experiments. --- .../characterization/qubit_spectroscopy.py | 133 +++--------------- .../resonator_spectroscopy.py | 38 ++--- .../library/characterization/spectroscopy.py | 128 +++++++++++++++++ 3 files changed, 155 insertions(+), 144 deletions(-) create mode 100644 qiskit_experiments/library/characterization/spectroscopy.py diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 803a528ee4..1d7bb375c3 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -12,21 +12,18 @@ """Spectroscopy experiment class.""" -from typing import Iterable, Optional, Tuple +from typing import Tuple 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 -from qiskit.qobj.utils import MeasLevel -from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.curve_analysis import ResonanceAnalysis +from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy -class QubitSpectroscopy(BaseExperiment): +class QubitSpectroscopy(Spectroscopy): """Class that runs spectroscopy by sweeping the qubit frequency. # section: overview @@ -50,79 +47,6 @@ class QubitSpectroscopy(BaseExperiment): __spec_gate_name__ = "Spec" - @classmethod - def _default_run_options(cls) -> Options: - """Default options values for the experiment :meth:`run` method.""" - options = super()._default_run_options() - - options.meas_level = MeasLevel.KERNELED - options.meas_return = "single" - - return options - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default option values used for the spectroscopy pulse. - - Experiment Options: - amp (float): The amplitude of the spectroscopy pulse. Defaults to 0.1. - duration (int): The duration of the spectroscopy pulse. Defaults to 1024 samples. - sigma (float): The standard deviation of the flanks of the spectroscopy pulse. - Defaults to 256. - width (int): The width of the flat-top part of the GaussianSquare pulse. - Defaults to 0. - """ - options = super()._default_experiment_options() - - options.amp = 0.1 - options.duration = 1024 - options.sigma = 256 - options.width = 0 - - return options - - def __init__( - self, - qubit: int, - frequencies: Iterable[float], - backend: Optional[Backend] = None, - absolute: bool = True, - ): - """ - A spectroscopy experiment run by setting the frequency of the qubit drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. - The spectroscopy pulse has the following parameters: - - amp: The amplitude of the pulse must be between 0 and 1, the default is 0.1. - - duration: The duration of the spectroscopy pulse in samples, the default is 1000 samples. - - sigma: The standard deviation of the pulse, the default is duration / 4. - - width: The width of the flat-top in the pulse, the default is 0, i.e. a Gaussian. - - Args: - qubit: The qubit on which to run spectroscopy. - frequencies: The frequencies to scan in the experiment, in Hz. - backend: Optional, the backend to run the experiment on. - absolute: Boolean to specify if the frequencies are absolute or relative to the - qubit frequency in the backend. - - Raises: - QiskitError: if there are less than three frequency shifts. - - """ - super().__init__([qubit], analysis=ResonanceAnalysis(), backend=backend) - - if len(frequencies) < 3: - raise QiskitError("Spectroscopy requires at least three frequencies.") - - self._frequencies = frequencies - self._absolute = absolute - - if not self._absolute: - self.analysis.set_options(xlabel="Frequency shift") - else: - self.analysis.set_options(xlabel="Frequency") - - self.analysis.set_options(ylabel="Signal [arb. unit]") - @property def center_frequency(self) -> float: """Returns the center frequency of the experiment. @@ -134,16 +58,22 @@ def center_frequency(self) -> float: QiskitError: If the experiment does not have a backend set. """ if self.backend is None: - raise QiskitError("backend not set. Cannot call center_frequency.") + raise QiskitError("backend not set. Cannot determine the center frequency.") return self.backend.defaults().qubit_freq_est[self.physical_qubits[0]] - def _spec_gate_schedule( - self, backend: Optional[Backend] = None - ) -> Tuple[pulse.ScheduleBlock, Parameter]: + 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 _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" freq_param = Parameter("frequency") - with pulse.build(backend=backend, name="spectroscopy") as schedule: + with pulse.build(backend=self.backend, name="spectroscopy") as schedule: pulse.shift_frequency(freq_param, pulse.DriveChannel(self.physical_qubits[0])) pulse.play( pulse.GaussianSquare( @@ -158,28 +88,6 @@ def _spec_gate_schedule( 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 _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): - """Helper method to add the metadata to avoid code duplication with subclasses.""" - circuit.metadata = { - "experiment_type": self._type, - "qubits": (self.physical_qubits[0],), - "xval": np.round(freq, decimals=3), - "unit": "Hz", - "amplitude": self.experiment_options.amp, - "duration": self.experiment_options.duration, - "sigma": self.experiment_options.sigma, - "width": self.experiment_options.width, - "schedule": str(sched), - } - def circuits(self): """Create the circuit for the spectroscopy experiment. @@ -188,17 +96,10 @@ def circuits(self): Returns: circuits: The circuits that will run the spectroscopy experiment. - - Raises: - QiskitError: If absolute frequencies are used but no backend is given. - QiskitError: If the backend configuration does not define dt. - AttributeError: If backend to run on does not contain 'dt' configuration. """ - if self.backend is None and self._absolute: - raise QiskitError("Cannot run spectroscopy in absolute without a backend.") # Create a template circuit - sched, freq_param = self._spec_gate_schedule(self.backend) + sched, freq_param = self._schedule() circuit = self._template_circuit(freq_param) circuit.add_calibration( self.__spec_gate_name__, self.physical_qubits, sched, params=[freq_param] @@ -207,9 +108,7 @@ def circuits(self): # Create the circuits to run circs = [] for freq in self._frequencies: - freq_shift = freq - if self._absolute: - freq_shift -= self.center_frequency + freq_shift = freq - self.center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 6003c3b538..009860c520 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -21,12 +21,12 @@ from qiskit.providers import Backend import qiskit.pulse as pulse -from qiskit_experiments.library.characterization import QubitSpectroscopy +from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy from qiskit_experiments.data_processing.processor_library import get_processor, ProjectorType from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis -class ResonatorSpectroscopy(QubitSpectroscopy): +class ResonatorSpectroscopy(Spectroscopy): """Perform spectroscopy on the readout resonator. # section: overview @@ -79,12 +79,10 @@ def __init__( resonator frequency in the backend. experiment_options: Key word arguments used to set the experiment options. """ - super().__init__(qubit, frequencies, backend, absolute) - self.analysis = ResonatorSpectroscopyAnalysis() + analysis = ResonatorSpectroscopyAnalysis() + super().__init__(qubit, frequencies, backend, absolute, analysis, **experiment_options) self._set_analysis_data_processor() - self.set_experiment_options(**experiment_options) - def _set_analysis_data_processor(self): """Keep the data processor consistent with the run options.""" processor = get_processor( @@ -115,23 +113,21 @@ def center_frequency(self) -> float: return self.backend.defaults().meas_freq_est[self.physical_qubits[0]] - def _meas_template_circuit(self) -> QuantumCircuit: + def _template_circuit(self) -> QuantumCircuit: """Return the template quantum circuit.""" circuit = QuantumCircuit(1, 1) circuit.measure(0, 0) return circuit - def _spec_gate_schedule( - self, backend: Optional[Backend] = None - ) -> Tuple[pulse.ScheduleBlock, Parameter]: + def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" qubit = self.physical_qubits[0] freq_param = Parameter("frequency") - with pulse.build(backend=backend, name="spectroscopy") as schedule: + with pulse.build(backend=self.backend, name="spectroscopy") as schedule: pulse.shift_frequency(freq_param, pulse.MeasureChannel(qubit)) pulse.play( pulse.GaussianSquare( @@ -150,33 +146,21 @@ def circuits(self): """Create the circuit for the spectroscopy experiment. The circuits are based on a GaussianSquare pulse and a frequency_shift instruction - encapsulated in a gate. + encapsulated in a measurement instruction. Returns: circuits: The circuits that will run the spectroscopy experiment. - - Raises: - QiskitError: If absolute frequencies are used but no backend is given. - QiskitError: If the backend configuration does not define dt. - AttributeError: If backend to run on does not contain 'dt' configuration. """ - if self.backend is None and self._absolute: - raise QiskitError("Cannot run spectroscopy in absolute without a backend.") - - # Create a template circuit - sched, freq_param = self._spec_gate_schedule(self.backend) + sched, freq_param = self._schedule() - # Create the circuits to run circs = [] for freq in self._frequencies: - freq_shift = freq - if self._absolute: - freq_shift -= self.center_frequency + freq_shift = freq - self.center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) sched_ = sched.assign_parameters({freq_param: freq_shift}, inplace=False) - circuit = self._meas_template_circuit() + circuit = self._template_circuit() circuit.add_calibration("measure", self.physical_qubits, sched_) self._add_metadata(circuit, freq, sched) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py new file mode 100644 index 0000000000..8d973cb561 --- /dev/null +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -0,0 +1,128 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Abstract spectroscopy experiment base class.""" + +from abc import ABC, abstractmethod +from typing import Iterable, Optional + +import numpy as np +import qiskit.pulse as pulse +from qiskit import QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.providers import Backend +from qiskit.qobj.utils import MeasLevel + +from qiskit_experiments.framework import BaseAnalysis, BaseExperiment, Options +from qiskit_experiments.curve_analysis import ResonanceAnalysis + + +class Spectroscopy(BaseExperiment, ABC): + """An abstract class for spectroscopy experiments.""" + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default option values used for the spectroscopy pulse. + + Experiment Options: + amp (float): The amplitude of the spectroscopy pulse. Defaults to 0.1 and must + be between 0 and 1. + duration (int): The duration of the spectroscopy pulse. Defaults to 1024 samples. + sigma (float): The standard deviation of the flanks of the spectroscopy pulse. + Defaults to 256. + width (int): The width of the flat-top part of the GaussianSquare pulse. + Defaults to 0. + """ + options = super()._default_experiment_options() + + options.amp = 0.1 + options.duration = 1024 + options.sigma = 256 + options.width = 0 + + return options + + @classmethod + def _default_run_options(cls) -> Options: + """Default options values for the experiment :meth:`run` method.""" + options = super()._default_run_options() + + options.meas_level = MeasLevel.KERNELED + options.meas_return = "single" + + return options + + def __init__( + self, + qubit: int, + frequencies: Iterable[float], + backend: Optional[Backend] = None, + absolute: bool = True, + analysis: Optional[BaseAnalysis] = None, + **experiment_options, + ): + """A spectroscopy experiment where the frequency of a pulse is scanned. + + Args: + qubit: The qubit on which to run spectroscopy. + frequencies: The frequencies to scan in the experiment, in Hz. + backend: Optional, the backend to run the experiment on. + absolute: Boolean to specify if the frequencies are absolute or relative to the + qubit frequency in the backend. + analysis: An instance of the analysis class to use. + experiment_options: Key word arguments used to set the experiment options. + + Raises: + QiskitError: if there are less than three frequency shifts. + + """ + analysis = ResonanceAnalysis() or analysis + + super().__init__([qubit], analysis=analysis, backend=backend) + + if len(frequencies) < 3: + raise QiskitError("Spectroscopy requires at least three frequencies.") + + self._frequencies = frequencies + self._absolute = absolute + + if not self._absolute: + self.analysis.set_options(xlabel="Frequency shift") + else: + self.analysis.set_options(xlabel="Frequency") + + self.analysis.set_options(ylabel="Signal [arb. unit]") + self.set_experiment_options(**experiment_options) + + @property + @abstractmethod + def center_frequency(self) -> float: + """Return the center frequency when running absolute frequencies. + + Spectroscopy experiments should implement schedules using frequency shifts. Therefore, + if an absolute frequency range is given the frequency shifts need to be correct by the + center frequency which depends on the nature of the spectroscopy experiment. + """ + + def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): + """Helper method to add the metadata to avoid code duplication with subclasses.""" + circuit.metadata = { + "experiment_type": self._type, + "qubits": self.physical_qubits, + "xval": np.round(freq, decimals=3), + "unit": "Hz", + "amplitude": self.experiment_options.amp, + "duration": self.experiment_options.duration, + "sigma": self.experiment_options.sigma, + "width": self.experiment_options.width, + "schedule": str(sched), + } From a2bc3da1f418060c5c1134f4ead251c4de69cad0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 16:33:26 +0100 Subject: [PATCH 20/68] * made center_frequency private. --- .../library/characterization/qubit_spectroscopy.py | 4 ++-- .../library/characterization/resonator_spectroscopy.py | 4 ++-- qiskit_experiments/library/characterization/spectroscopy.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 1d7bb375c3..ae5fe1a395 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -48,7 +48,7 @@ class QubitSpectroscopy(Spectroscopy): __spec_gate_name__ = "Spec" @property - def center_frequency(self) -> float: + def _center_frequency(self) -> float: """Returns the center frequency of the experiment. Returns: @@ -108,7 +108,7 @@ def circuits(self): # Create the circuits to run circs = [] for freq in self._frequencies: - freq_shift = freq - self.center_frequency if self._absolute else freq + freq_shift = freq - self._center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 009860c520..0d7c236349 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -99,7 +99,7 @@ def set_run_options(self, **fields): self._set_analysis_data_processor() @property - def center_frequency(self) -> float: + def _center_frequency(self) -> float: """Returns the center frequency of the experiment. Returns: @@ -155,7 +155,7 @@ def circuits(self): circs = [] for freq in self._frequencies: - freq_shift = freq - self.center_frequency if self._absolute else freq + freq_shift = freq - self._center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) sched_ = sched.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 8d973cb561..9c67777545 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -105,7 +105,7 @@ def __init__( @property @abstractmethod - def center_frequency(self) -> float: + def _center_frequency(self) -> float: """Return the center frequency when running absolute frequencies. Spectroscopy experiments should implement schedules using frequency shifts. Therefore, From 8bdce88cf0f5998c874f4332d85a375479e81483 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 16:46:10 +0100 Subject: [PATCH 21/68] * Center frequency refactor. --- .../characterization/qubit_spectroscopy.py | 4 ++-- .../characterization/resonator_spectroscopy.py | 4 ++-- .../library/characterization/spectroscopy.py | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index ae5fe1a395..49bd8414c2 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -48,7 +48,7 @@ class QubitSpectroscopy(Spectroscopy): __spec_gate_name__ = "Spec" @property - def _center_frequency(self) -> float: + def _backend_center_frequency(self) -> float: """Returns the center frequency of the experiment. Returns: @@ -108,7 +108,7 @@ def circuits(self): # Create the circuits to run circs = [] for freq in self._frequencies: - freq_shift = freq - self._center_frequency if self._absolute else freq + freq_shift = freq - self._backend_center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 0d7c236349..b98b0c27e7 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -99,7 +99,7 @@ def set_run_options(self, **fields): self._set_analysis_data_processor() @property - def _center_frequency(self) -> float: + def _backend_center_frequency(self) -> float: """Returns the center frequency of the experiment. Returns: @@ -155,7 +155,7 @@ def circuits(self): circs = [] for freq in self._frequencies: - freq_shift = freq - self._center_frequency if self._absolute else freq + freq_shift = freq - self._backend_center_frequency if self._absolute else freq freq_shift = np.round(freq_shift, decimals=3) sched_ = sched.assign_parameters({freq_param: freq_shift}, inplace=False) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 9c67777545..f7c6e425e8 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -105,7 +105,7 @@ def __init__( @property @abstractmethod - def _center_frequency(self) -> float: + def _backend_center_frequency(self) -> float: """Return the center frequency when running absolute frequencies. Spectroscopy experiments should implement schedules using frequency shifts. Therefore, @@ -113,6 +113,21 @@ def _center_frequency(self) -> float: center frequency which depends on the nature of the spectroscopy experiment. """ + @property + def center_frequency(self) -> float: + """The center frequency of the spectroscopy experiment. + + Returns: + The center frequency in absolute terms. If the experiment is running in absolute mode + then we return the mid-point of the frequency range. However, if the experiment is + running in relative mode then we return the frequency of the backend with respect to + which the relative shift is applied. + """ + if self._absolute: + return (max(self._frequencies) - min(self._frequencies)) / 2 + else: + return self._backend_center_frequency + def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): """Helper method to add the metadata to avoid code duplication with subclasses.""" circuit.metadata = { From e9f7ae4ff86885f385408b45fd5ce1924698c34c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 20 Jan 2022 16:53:22 +0100 Subject: [PATCH 22/68] * circuit metadata refactor. --- qiskit_experiments/library/characterization/spectroscopy.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index f7c6e425e8..01615c4a8e 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -135,9 +135,5 @@ def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.Sched "qubits": self.physical_qubits, "xval": np.round(freq, decimals=3), "unit": "Hz", - "amplitude": self.experiment_options.amp, - "duration": self.experiment_options.duration, - "sigma": self.experiment_options.sigma, - "width": self.experiment_options.width, "schedule": str(sched), } From dfe314978558e16c320a568ef3d44da486a2fa00 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 Jan 2022 09:07:57 +0100 Subject: [PATCH 23/68] * Added acquire delay and set default amplitude to 1 for resonator spec. --- .../resonator_spectroscopy.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index b98b0c27e7..2d6eb1b562 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -21,6 +21,7 @@ from qiskit.providers import Backend import qiskit.pulse as pulse +from qiskit_experiments.framework import Options from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy from qiskit_experiments.data_processing.processor_library import get_processor, ProjectorType from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis @@ -54,6 +55,24 @@ class ResonatorSpectroscopy(Spectroscopy): qiskit_experiments.library.characterization.qubit_spectroscopy.QubitSpectroscopy """ + @classmethod + def _default_experiment_options(cls) -> Options: + """Default option values used for the spectroscopy pulse. + + Experiment Options: + amp (float): The amplitude of the spectroscopy pulse. Defaults to 1 and must + be between 0 and 1. + acquisition_duration (int): The duration of the acquisition instruction. By default + is lasts 1024 samples, i.e. the same duration as the measurement pulse. + """ + options = super()._default_experiment_options() + + options.amp = 1 + options.acquire_duration = 1024 + options.acquire_delay = 0 + + return options + def __init__( self, qubit: int, @@ -138,7 +157,10 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: ), pulse.MeasureChannel(qubit), ) - pulse.acquire(self.experiment_options.duration, qubit, pulse.MemorySlot(0)) + + with pulse.align_left(): + pulse.delay(self.experiment_options.acquire_delay, pulse.AcquireChannel(qubit)) + pulse.acquire(self.experiment_options.acquire_duration, qubit, pulse.MemorySlot(0)) return schedule, freq_param From b5bce858f7f498b362e90fa801f0ab093ca59ce8 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 Jan 2022 17:24:47 +0100 Subject: [PATCH 24/68] * Reonator spectroscopy test Lorentzian resonance. --- test/test_resonator_spectroscopy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index a87ff7b50a..79792c7a49 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -46,7 +46,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency delta_freq = freq_shift - self._freq_offset - return np.exp(-(delta_freq ** 2) / (2 * self._linewidth ** 2)) + return (0.5 * self._linewidth) ** 2 / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) def _iq_phase(self, circuit: QuantumCircuit) -> float: """Add a phase to the IQ point depending on how far we are from the resonance. @@ -78,8 +78,7 @@ def test_end_to_end(self): result = expdata.analysis_results(1) value = result.value.value - self.assertTrue(6.60999e9 < value < 6.61001e9) - self.assertEqual(result.quality, "good") + self.assertTrue(6.6099e9 < value < 6.6101e9) def test_experiment_config(self): """Test converting to and from config works""" From 6d487cc53135fbda0a35af65b4532a9829fbde6b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 21 Jan 2022 17:38:20 +0100 Subject: [PATCH 25/68] * IQ plot robustness. --- .../analysis/resonator_spectroscopy_analysis.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index e21026520d..280463958e 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -53,13 +53,14 @@ def _run_analysis( if len(mem.shape) == 3: iqs.append(np.average(mem.reshape(mem.shape[0], mem.shape[2]), axis=0)) - iqs = np.array(iqs) - axis.scatter(iqs[:, 0], iqs[:, 1], color="b") - axis.set_xlabel("In phase [arb. units]", fontsize=self.options.style.axis_label_size) - axis.set_ylabel("Quadrature [arb. units]", fontsize=self.options.style.axis_label_size) - axis.tick_params(labelsize=self.options.style.tick_label_size) - axis.grid(True) + if len(iqs) > 0: + iqs = np.array(iqs) + axis.scatter(iqs[:, 0], iqs[:, 1], color="b") + axis.set_xlabel("In phase [arb. units]", fontsize=self.options.style.axis_label_size) + axis.set_ylabel("Quadrature [arb. units]", fontsize=self.options.style.axis_label_size) + axis.tick_params(labelsize=self.options.style.tick_label_size) + axis.grid(True) - figures.append(figure) + figures.append(figure) return analysis_results, figures From 0ef184befc56130e3b248a4a1ad88e5e44f66e44 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 11:48:35 +0100 Subject: [PATCH 26/68] * Refactored ResonatorSpectroscopyAnalysis --- .../analysis/resonator_spectroscopy_analysis.py | 3 ++- .../characterization/resonator_spectroscopy.py | 16 ---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 280463958e..2776791103 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -19,6 +19,7 @@ from qiskit_experiments.curve_analysis import ResonanceAnalysis from qiskit_experiments.framework import AnalysisResultData, ExperimentData from qiskit_experiments.framework.matplotlib import get_non_gui_ax +from qiskit_experiments.data_processing.processor_library import ProjectorType class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): @@ -27,7 +28,7 @@ class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): @classmethod def _default_options(cls): options = super()._default_options() - options.dimensionality_reduction = "ToAbs" + options.dimensionality_reduction = ProjectorType.ABS options.result_parameters = [curve.ParameterRepr("freq", "meas_freq", "Hz")] options.plot_iq_data = True return options diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 2d6eb1b562..7e6c166757 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -100,22 +100,6 @@ def __init__( """ analysis = ResonatorSpectroscopyAnalysis() super().__init__(qubit, frequencies, backend, absolute, analysis, **experiment_options) - self._set_analysis_data_processor() - - def _set_analysis_data_processor(self): - """Keep the data processor consistent with the run options.""" - processor = get_processor( - self.run_options.meas_level, - self.run_options.meas_return, - self.analysis.options.normalization, - dimensionality_reduction=ProjectorType.ABS, - ) - self.analysis.set_options(data_processor=processor) - - def set_run_options(self, **fields): - """Wrap set run options to keep the data processor consistent.""" - super().set_run_options(**fields) - self._set_analysis_data_processor() @property def _backend_center_frequency(self) -> float: From bab4a73c61b82c9d3ca45fa05c823e448b1eb58b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 12:05:02 +0100 Subject: [PATCH 27/68] * Test fix. --- test/test_resonator_spectroscopy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 79792c7a49..1fa2975861 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -85,9 +85,9 @@ def test_experiment_config(self): exp = ResonatorSpectroscopy(1, np.linspace(100, 150, 20) * 1e6) loaded_exp = ResonatorSpectroscopy.from_config(exp.config()) self.assertNotEqual(exp, loaded_exp) - self.assertTrue(self.experiments_equiv(exp, loaded_exp)) + self.assertTrue(self.json_equiv(exp, loaded_exp)) def test_roundtrip_serializable(self): """Test round trip JSON serialization""" exp = ResonatorSpectroscopy(1, np.linspace(int(100e6), int(150e6), int(20e6))) - self.assertRoundTripSerializable(exp, self.experiments_equiv) + self.assertRoundTripSerializable(exp, self.json_equiv) From 13d5bb2be2b94867f0b78c90a402211dadf0c850 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 13:59:22 +0100 Subject: [PATCH 28/68] * Units, corresponding test fix, and black. --- .../data_processing/processor_library.py | 1 + .../resonator_spectroscopy_analysis.py | 8 +++- .../resonator_spectroscopy.py | 45 ++++++++++++++++--- .../library/characterization/spectroscopy.py | 2 +- test/test_resonator_spectroscopy.py | 1 + 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index 29b2059c9e..f0e34dec2e 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -24,6 +24,7 @@ class ProjectorType(Enum): """Types of projectors for data dimensionality reduction.""" + SVD = nodes.SVD ABS = nodes.ToAbs REAL = nodes.ToReal diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 2776791103..3167b69a82 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -57,8 +57,12 @@ def _run_analysis( if len(iqs) > 0: iqs = np.array(iqs) axis.scatter(iqs[:, 0], iqs[:, 1], color="b") - axis.set_xlabel("In phase [arb. units]", fontsize=self.options.style.axis_label_size) - axis.set_ylabel("Quadrature [arb. units]", fontsize=self.options.style.axis_label_size) + axis.set_xlabel( + "In phase [arb. units]", fontsize=self.options.style.axis_label_size + ) + axis.set_ylabel( + "Quadrature [arb. units]", fontsize=self.options.style.axis_label_size + ) axis.tick_params(labelsize=self.options.style.tick_label_size) axis.grid(True) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 7e6c166757..9230b03776 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -23,7 +23,6 @@ from qiskit_experiments.framework import Options from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy -from qiskit_experiments.data_processing.processor_library import get_processor, ProjectorType from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis @@ -68,8 +67,11 @@ def _default_experiment_options(cls) -> Options: options = super()._default_experiment_options() options.amp = 1 - options.acquire_duration = 1024 + options.acquire_duration = 240e-9 options.acquire_delay = 0 + options.unit = "s" + options.duration = 240e-9 + options.sigma = 60e-9 return options @@ -126,6 +128,35 @@ def _template_circuit(self) -> QuantumCircuit: def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" + unit = self.experiment_options.unit + if unit not in ["s", "dt"]: + raise QiskitError(f"Unrecognized unit: {unit}.") + + if unit == "s": + dt = getattr(self.backend.configuration(), "dt", None) + constraints = getattr(self.backend.configuration(), "timing_constraints", {}) + granularity = constraints.get("granularity", None) + + if dt is None or granularity is None: + raise QiskitError( + f"{self.__class__.__name__} requires both dt and sample granularity if " + f"units are s. Founds {dt} and {granularity}, respectively." + ) + + acq_dur = int( + granularity * (self.experiment_options.acquire_duration / dt // granularity) + ) + acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) + duration = int(granularity * (self.experiment_options.duration / dt // granularity)) + sigma = granularity * (self.experiment_options.sigma / dt // granularity) + width = granularity * (self.experiment_options.width / dt // granularity) + else: + acq_dur = self.experiment_options.acquire_duration + acq_del = self.experiment_options.acquire_delay + duration = self.experiment_options.duration + sigma = self.experiment_options.sigma + width = self.experiment_options.width + qubit = self.physical_qubits[0] freq_param = Parameter("frequency") @@ -134,17 +165,17 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: pulse.shift_frequency(freq_param, pulse.MeasureChannel(qubit)) pulse.play( pulse.GaussianSquare( - duration=self.experiment_options.duration, + duration=duration, amp=self.experiment_options.amp, - sigma=self.experiment_options.sigma, - width=self.experiment_options.width, + sigma=sigma, + width=width, ), pulse.MeasureChannel(qubit), ) with pulse.align_left(): - pulse.delay(self.experiment_options.acquire_delay, pulse.AcquireChannel(qubit)) - pulse.acquire(self.experiment_options.acquire_duration, qubit, pulse.MemorySlot(0)) + pulse.delay(acq_del, pulse.AcquireChannel(qubit)) + pulse.acquire(acq_dur, qubit, pulse.MemorySlot(0)) return schedule, freq_param diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 01615c4a8e..065ce17bdc 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -85,7 +85,7 @@ def __init__( QiskitError: if there are less than three frequency shifts. """ - analysis = ResonanceAnalysis() or analysis + analysis = analysis or ResonanceAnalysis() super().__init__([qubit], analysis=analysis, backend=backend) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 1fa2975861..26793e3d4a 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -40,6 +40,7 @@ def __init__( self._freq_offset = freq_offset super().__init__(iq_cluster_centers, iq_cluster_width) + self._configuration.timing_constraints = {"granularity": 16} def _compute_probability(self, circuit: QuantumCircuit) -> float: """Returns the probability based on the frequency.""" From cd436f8321904d847e2646154b1c86602153ec2e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 14:20:55 +0100 Subject: [PATCH 29/68] * Lint and default frequencies. --- qiskit_experiments/data_processing/nodes.py | 1 + .../resonator_spectroscopy.py | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 60c2a7b548..aa5fb14197 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -386,6 +386,7 @@ def _process(self, data: np.array) -> np.array: Returns: A N-1 dimensional array, each entry is the absolute value of the given IQ data. """ + # pylint: disable=no-member return unp.sqrt(data[..., 0] ** 2 + data[..., 1] ** 2) * self.scale diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 9230b03776..2b7e618d01 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -78,8 +78,8 @@ def _default_experiment_options(cls) -> Options: def __init__( self, qubit: int, - frequencies: Iterable[float], backend: Optional[Backend] = None, + frequencies: Optional[Iterable[float]] = None, absolute: bool = True, **experiment_options, ): @@ -94,13 +94,30 @@ def __init__( Args: qubit: The qubit on which to run readout spectroscopy. - frequencies: The frequencies to scan in the experiment, in Hz. backend: Optional, the backend to run the experiment on. + frequencies: The frequencies to scan in the experiment, in Hz. absolute: Boolean to specify if the frequencies are absolute or relative to the resonator frequency in the backend. experiment_options: Key word arguments used to set the experiment options. + + Raises: + QiskitError: if no frequencies are given and absolute frequencies are desired and + no backend is given. """ analysis = ResonatorSpectroscopyAnalysis() + + if frequencies is None: + frequencies = np.linspace(-20.0, 20.0, 51) + + if absolute: + if backend is None: + raise QiskitError( + "Cannot automatically compute absolute frequencies without a backend." + ) + + center_freq = backend.defaults().meas_freq_est[qubit] + frequencies += center_freq + super().__init__(qubit, frequencies, backend, absolute, analysis, **experiment_options) @property From 58f95352a711a08b35d195b7cee959b0741ba90c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 14:26:23 +0100 Subject: [PATCH 30/68] * Fix factor omission. --- .../library/characterization/resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 2b7e618d01..761ea4e2d7 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -107,7 +107,7 @@ def __init__( analysis = ResonatorSpectroscopyAnalysis() if frequencies is None: - frequencies = np.linspace(-20.0, 20.0, 51) + frequencies = np.linspace(-20.0e6, 20.0e6, 51) if absolute: if backend is None: From 256c3f649a9c28619039cc48ba6836297e8786e2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 24 Jan 2022 14:40:44 +0100 Subject: [PATCH 31/68] * Align tests. --- test/test_resonator_spectroscopy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 26793e3d4a..8b8f96e07e 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -73,7 +73,7 @@ def test_end_to_end(self): res_freq = backend.defaults().meas_freq_est[qubit] frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 51) - spec = ResonatorSpectroscopy(qubit, frequencies=frequencies, backend=backend) + spec = ResonatorSpectroscopy(qubit, backend=backend, frequencies=frequencies) expdata = spec.run(backend) result = expdata.analysis_results(1) @@ -83,12 +83,12 @@ def test_end_to_end(self): def test_experiment_config(self): """Test converting to and from config works""" - exp = ResonatorSpectroscopy(1, np.linspace(100, 150, 20) * 1e6) + exp = ResonatorSpectroscopy(1, frequencies=np.linspace(100, 150, 20) * 1e6) loaded_exp = ResonatorSpectroscopy.from_config(exp.config()) self.assertNotEqual(exp, loaded_exp) self.assertTrue(self.json_equiv(exp, loaded_exp)) def test_roundtrip_serializable(self): """Test round trip JSON serialization""" - exp = ResonatorSpectroscopy(1, np.linspace(int(100e6), int(150e6), int(20e6))) + exp = ResonatorSpectroscopy(1, frequencies=np.linspace(int(100e6), int(150e6), int(20e6))) self.assertRoundTripSerializable(exp, self.json_equiv) From 86001af821c147b15d240d361c0f1f1e4be23308 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 11:40:21 +0100 Subject: [PATCH 32/68] * Simplified resonator spec options. --- .../resonator_spectroscopy.py | 59 ++++++++----------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 761ea4e2d7..db936cf4e0 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -58,18 +58,25 @@ class ResonatorSpectroscopy(Spectroscopy): def _default_experiment_options(cls) -> Options: """Default option values used for the spectroscopy pulse. + All units of the resonator spectroscopy experiment are given in seconds. + Experiment Options: amp (float): The amplitude of the spectroscopy pulse. Defaults to 1 and must be between 0 and 1. - acquisition_duration (int): The duration of the acquisition instruction. By default - is lasts 1024 samples, i.e. the same duration as the measurement pulse. + acquire_duration (float): The duration of the acquisition instruction. By + default is lasts 240 ns, i.e. the same duration as the measurement pulse. + acquire_delay (float): The duration by which to delay the acquire instruction + with respect to the measurement pulse. + duration (float): The duration in seconds of the spectroscopy pulse. + sigma (float): The standard deviation of the spectroscopy pulse in seconds. + width (float): The width of the flat-top part of the GaussianSquare pulse in + seconds. Defaults to 0. """ options = super()._default_experiment_options() options.amp = 1 options.acquire_duration = 240e-9 options.acquire_delay = 0 - options.unit = "s" options.duration = 240e-9 options.sigma = 60e-9 @@ -85,12 +92,8 @@ def __init__( ): """ A spectroscopy experiment run by setting the frequency of the readout drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time. - The spectroscopy pulse has the following parameters: - - amp: The amplitude of the pulse must be between 0 and 1, the default is 0.1. - - duration: The duration of the spectroscopy pulse in samples, the default is 1000 samples. - - sigma: The standard deviation of the pulse, the default is duration / 4. - - width: The width of the flat-top in the pulse, the default is 0, i.e. a Gaussian. + The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time + through the experiment options. Args: qubit: The qubit on which to run readout spectroscopy. @@ -145,34 +148,20 @@ def _template_circuit(self) -> QuantumCircuit: def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" - unit = self.experiment_options.unit - if unit not in ["s", "dt"]: - raise QiskitError(f"Unrecognized unit: {unit}.") - - if unit == "s": - dt = getattr(self.backend.configuration(), "dt", None) - constraints = getattr(self.backend.configuration(), "timing_constraints", {}) - granularity = constraints.get("granularity", None) + dt = getattr(self.backend.configuration(), "dt", None) + constraints = getattr(self.backend.configuration(), "timing_constraints", {}) + granularity = constraints.get("granularity", None) - if dt is None or granularity is None: - raise QiskitError( - f"{self.__class__.__name__} requires both dt and sample granularity if " - f"units are s. Founds {dt} and {granularity}, respectively." - ) + if dt is None or granularity is None: + raise QiskitError(f"{self.__class__.__name__} needs both dt and sample granularity.") - acq_dur = int( - granularity * (self.experiment_options.acquire_duration / dt // granularity) - ) - acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) - duration = int(granularity * (self.experiment_options.duration / dt // granularity)) - sigma = granularity * (self.experiment_options.sigma / dt // granularity) - width = granularity * (self.experiment_options.width / dt // granularity) - else: - acq_dur = self.experiment_options.acquire_duration - acq_del = self.experiment_options.acquire_delay - duration = self.experiment_options.duration - sigma = self.experiment_options.sigma - width = self.experiment_options.width + acq_dur = int( + granularity * (self.experiment_options.acquire_duration / dt // granularity) + ) + acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) + duration = int(granularity * (self.experiment_options.duration / dt // granularity)) + sigma = granularity * (self.experiment_options.sigma / dt // granularity) + width = granularity * (self.experiment_options.width / dt // granularity) qubit = self.physical_qubits[0] From 67865cd977de1215662a46f8cf9daadb78048a83 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 11:44:13 +0100 Subject: [PATCH 33/68] * Reno. --- .../resonator-spectroscopy-89f790412838ba5b.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml index 70dcbd0b4f..28a6d34c62 100644 --- a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml +++ b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml @@ -4,9 +4,12 @@ features: This change introduces the new experiment :py:class:`~qiskit_experiments.library.ResonatorSpectroscopy` to run spectroscopy on readout resonators. This is done by attaching a custom pulse-schedule to - the measure instruction. Furthermore, a new data processing node + the measure instruction. Note that the resonator spectroscopy experiment may + cause errors on backends that do not support circuit instructions with measurement + schedules attached to them. + - | + Furthermore, a new data processing node :py:class`~qiskit_experiments.data_processing.nodes.ToAbs` is introduced to take the absolute value of the IQ points. This node is needed to analyse readout - resonator spectroscopy IQ data since it rotates around in the IQ plane. Note that - the resonator spectroscopy experiment may cause errors on backends that do not - support circuit instructions with measurement schedules attached to them. + resonator spectroscopy IQ data since it rotates around in the IQ plane but can + also be used in other contexts. From 7c44dd165a5be334d3a2ccddf7f9c1e4ce3b4509 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 11:57:59 +0100 Subject: [PATCH 34/68] * More robust IQ plotting. --- .../analysis/resonator_spectroscopy_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 3167b69a82..533c4656dd 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -52,7 +52,8 @@ def _run_analysis( # Average single-shot data. if len(mem.shape) == 3: - iqs.append(np.average(mem.reshape(mem.shape[0], mem.shape[2]), axis=0)) + for idx in range(mem.shape[1]): + iqs.append(np.average(mem[:, idx, :], axis=0)) if len(iqs) > 0: iqs = np.array(iqs) From b8b99d6c471a3cb0c1bebbfd025e14ef45aeaee3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 12:03:43 +0100 Subject: [PATCH 35/68] * Y-axis label. --- qiskit_experiments/library/characterization/spectroscopy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 065ce17bdc..92ff13cf53 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -100,7 +100,6 @@ def __init__( else: self.analysis.set_options(xlabel="Frequency") - self.analysis.set_options(ylabel="Signal [arb. unit]") self.set_experiment_options(**experiment_options) @property From 26848e38825f31e36cfea195f4af1a08e63d100b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 12:08:33 +0100 Subject: [PATCH 36/68] * Resonator spec doc string. --- .../characterization/resonator_spectroscopy.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index db936cf4e0..240336de5c 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -41,11 +41,13 @@ class ResonatorSpectroscopy(Spectroscopy): c: 1/═╩═ 0 - where a spectroscopy pulse is attached to the measurement instruction. When doing - readout resonator spectroscopy, each measured IQ point has a frequency dependent - phase. Close to the resonance, the IQ points start rotating around in the IQ plan. - This effect must be accounted for in the data processing to produce a meaningful - signal. + where a spectroscopy pulse is attached to the measurement instruction. + + Side note: when doing readout resonator spectroscopy, each measured IQ point has a + frequency dependent phase. Close to the resonance, the IQ points start rotating around + in the IQ plan. This effect must be accounted for in the data processing to produce a + meaningful signal. The default data processing workflow will therefore reduce the two- + dimensional IQ data to one-dimensional data using the magnitude of each IQ point. # section: analysis_ref :py:class:`ResonatorSpectroscopyAnalysis` From 373dfd339995847d98a883b45c4b352d8e953426 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 12:15:09 +0100 Subject: [PATCH 37/68] * Resonator spec doc. --- .../library/characterization/resonator_spectroscopy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 240336de5c..8c8288cdc7 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -92,7 +92,8 @@ def __init__( absolute: bool = True, **experiment_options, ): - """ + """Initialize a resonator spectroscopy experiment. + A spectroscopy experiment run by setting the frequency of the readout drive. The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time through the experiment options. @@ -100,9 +101,12 @@ def __init__( Args: qubit: The qubit on which to run readout spectroscopy. backend: Optional, the backend to run the experiment on. - frequencies: The frequencies to scan in the experiment, in Hz. + frequencies: The frequencies to scan in the experiment, in Hz. The default values + range from -20 MHz to 20 MHz in 51 steps. If the ``absolute`` variable is + set to True then a center frequency obtained from the backend's defaults is + added to each value of this range. absolute: Boolean to specify if the frequencies are absolute or relative to the - resonator frequency in the backend. + resonator frequency in the backend. The default value is True. experiment_options: Key word arguments used to set the experiment options. Raises: From 7d7fa8eb02eb00b4cf20df73642fa6339e035fb4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 12:21:06 +0100 Subject: [PATCH 38/68] * Docstring. --- .../library/characterization/resonator_spectroscopy.py | 2 +- .../library/characterization/spectroscopy.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 8c8288cdc7..8fcb515900 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -93,7 +93,7 @@ def __init__( **experiment_options, ): """Initialize a resonator spectroscopy experiment. - + A spectroscopy experiment run by setting the frequency of the readout drive. The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time through the experiment options. diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 92ff13cf53..96c765679a 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -105,11 +105,13 @@ def __init__( @property @abstractmethod def _backend_center_frequency(self) -> float: - """Return the center frequency when running absolute frequencies. + """The default frequency for the channel of the spectroscopy pulse. - Spectroscopy experiments should implement schedules using frequency shifts. Therefore, - if an absolute frequency range is given the frequency shifts need to be correct by the - center frequency which depends on the nature of the spectroscopy experiment. + This frequency is used to calculate the appropriate frequency shifts to apply to the + spectroscopy pulse as its frequency is scanned in the experiment. Spectroscopy experiments + should implement schedules using frequency shifts. Therefore, if an absolute frequency + range is given the frequency shifts need to be corrected by the backend default frequency + which depends on the nature of the spectroscopy experiment. """ @property From a357bf0850da04e101430fb041b696904e314e4c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 14:04:54 +0100 Subject: [PATCH 39/68] * Metadata always has absolute frequencies. --- .../library/characterization/spectroscopy.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 96c765679a..6d4c3dab31 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -95,11 +95,6 @@ def __init__( self._frequencies = frequencies self._absolute = absolute - if not self._absolute: - self.analysis.set_options(xlabel="Frequency shift") - else: - self.analysis.set_options(xlabel="Frequency") - self.set_experiment_options(**experiment_options) @property @@ -131,6 +126,10 @@ def center_frequency(self) -> float: def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): """Helper method to add the metadata to avoid code duplication with subclasses.""" + + if not self._absolute: + freq += self._backend_center_frequency + circuit.metadata = { "experiment_type": self._type, "qubits": self.physical_qubits, From 62ce701f24075371a53068203c68d39f91a9269e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 14:09:16 +0100 Subject: [PATCH 40/68] * Moved projector type. --- qiskit_experiments/data_processing/nodes.py | 10 ++++++++++ .../data_processing/processor_library.py | 12 +----------- .../analysis/resonator_spectroscopy_analysis.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index aa5fb14197..943ceae8a2 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,6 +13,7 @@ """Different data analysis steps.""" from abc import abstractmethod +from enum import Enum from numbers import Number from typing import List, Union, Sequence @@ -571,3 +572,12 @@ def _process(self, data: np.ndarray) -> np.ndarray: The data that has been processed. """ return 2 * (0.5 - data) + + +class ProjectorType(Enum): + """Types of projectors for data dimensionality reduction.""" + + SVD = SVD + ABS = ToAbs + REAL = ToReal + IMAG = ToImag diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index f0e34dec2e..3e48eb56f1 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -12,25 +12,15 @@ """A collection of functions that return various data processors.""" -from enum import Enum - from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit_experiments.framework import ExperimentData, Options from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.nodes import ProjectorType from qiskit_experiments.data_processing import nodes -class ProjectorType(Enum): - """Types of projectors for data dimensionality reduction.""" - - SVD = nodes.SVD - ABS = nodes.ToAbs - REAL = nodes.ToReal - IMAG = nodes.ToImag - - def get_processor( experiment_data: ExperimentData, analysis_options: Options, diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 533c4656dd..e8ff74a639 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -19,7 +19,7 @@ from qiskit_experiments.curve_analysis import ResonanceAnalysis from qiskit_experiments.framework import AnalysisResultData, ExperimentData from qiskit_experiments.framework.matplotlib import get_non_gui_ax -from qiskit_experiments.data_processing.processor_library import ProjectorType +from qiskit_experiments.data_processing.nodes import ProjectorType class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): From 75243597bd0ebe16535c7e9517c5a0e3ab105e33 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 14:33:36 +0100 Subject: [PATCH 41/68] * Refactor resonance analysis to use a Lorentzian and renamed the existing ResonanceAnalysis to GaussianAnalysis. --- qiskit_experiments/curve_analysis/__init__.py | 3 + .../curve_analysis/fit_function.py | 11 ++ .../standard_analysis/__init__.py | 1 + .../standard_analysis/gaussian.py | 156 ++++++++++++++++++ .../standard_analysis/resonance.py | 45 +++-- 5 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 qiskit_experiments/curve_analysis/standard_analysis/gaussian.py diff --git a/qiskit_experiments/curve_analysis/__init__.py b/qiskit_experiments/curve_analysis/__init__.py index 5573c85b93..2272883672 100644 --- a/qiskit_experiments/curve_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/__init__.py @@ -48,6 +48,7 @@ DumpedOscillationAnalysis OscillationAnalysis ResonanceAnalysis + GaussianAnalysis ErrorAmplificationAnalysis Functions @@ -73,6 +74,7 @@ fit_function.cos_decay fit_function.exponential_decay fit_function.gaussian + fit_function.lorentzian fit_function.sin fit_function.sin_decay fit_function.bloch_oscillation_x @@ -120,5 +122,6 @@ DumpedOscillationAnalysis, OscillationAnalysis, ResonanceAnalysis, + GaussianAnalysis, ErrorAmplificationAnalysis, ) diff --git a/qiskit_experiments/curve_analysis/fit_function.py b/qiskit_experiments/curve_analysis/fit_function.py index 0a5cff4b78..8156296c3b 100644 --- a/qiskit_experiments/curve_analysis/fit_function.py +++ b/qiskit_experiments/curve_analysis/fit_function.py @@ -77,6 +77,17 @@ def gaussian( return amp * np.exp(-((x - x0) ** 2) / (2 * sigma ** 2)) + baseline +def lorentzian( + x: np.ndarray, amp: float = 1.0, gamma: float = 1.0, x0: float = 0.0, baseline: float = 0.0 +) -> np.ndarry: + r"""Lorentzian function + + .. math:: + y = \frac{\rm amp}{2\pi}\frac{\gamma}{(x - x0)^2 + (\gamma/2)^2} + {\rm baseline} + """ + return amp * gamma / ((x - x0) ** 2 + (gamma / 2) ** 2) / (2 * np.pi) + baseline + + def cos_decay( x: np.ndarray, amp: float = 1.0, diff --git a/qiskit_experiments/curve_analysis/standard_analysis/__init__.py b/qiskit_experiments/curve_analysis/standard_analysis/__init__.py index 769136bf02..fef9bfb26f 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/__init__.py @@ -14,5 +14,6 @@ from .oscillation import OscillationAnalysis, DumpedOscillationAnalysis from .resonance import ResonanceAnalysis +from .gaussian import GaussianAnalysis from .error_amplification_analysis import ErrorAmplificationAnalysis from .decay import DecayAnalysis diff --git a/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py new file mode 100644 index 0000000000..d8d61d72b3 --- /dev/null +++ b/qiskit_experiments/curve_analysis/standard_analysis/gaussian.py @@ -0,0 +1,156 @@ +# 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. + +"""Resonance analysis class based on a Gaussian fit.""" + +from typing import List, Union + +import numpy as np + +import qiskit_experiments.curve_analysis as curve +from qiskit_experiments.framework import Options + + +class GaussianAnalysis(curve.CurveAnalysis): + r"""A class to analyze a resonance, typically seen as a peak. + + Overview + This analysis takes only single series. This series is fit by the Gaussian function. + + Fit Model + The fit is based on the following Gaussian function. + + .. math:: + + F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + + Fit Parameters + - :math:`a`: Peak height. + - :math:`b`: Base line. + - :math:`f`: Center frequency. This is the fit parameter of main interest. + - :math:`\sigma`: Standard deviation of Gaussian function. + + Initial Guesses + - :math:`a`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. + - :math:`b`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.\ + constant_spectral_offset`. + - :math:`f`: Frequency at max height position calculated by + :func:`~qiskit_experiments.curve_analysis.guess.max_height`. + - :math:`\sigma`: Calculated from FWHM of peak :math:`w` + such that :math:`w / \sqrt{8} \ln{2}`, where FWHM is calculated by + :func:`~qiskit_experiments.curve_analysis.guess.full_width_half_max`. + + Bounds + - :math:`a`: [-2, 2] scaled with maximum signal value. + - :math:`b`: [-1, 1] scaled with maximum signal value. + - :math:`f`: [min(x), max(x)] of frequency scan range. + - :math:`\sigma`: [0, :math:`\Delta x`] where :math:`\Delta x` + represents frequency scan range. + + """ + + __series__ = [ + curve.SeriesDef( + fit_func=lambda x, a, sigma, freq, b: curve.fit_function.gaussian( + x, amp=a, sigma=sigma, x0=freq, baseline=b + ), + plot_color="blue", + model_description=r"a \exp(-(x-f)^2/(2\sigma^2)) + b", + ) + ] + + @classmethod + def _default_options(cls) -> Options: + options = super()._default_options() + options.result_parameters = [curve.ParameterRepr("freq", "f01", "Hz")] + options.normalization = True + options.xlabel = "Frequency" + options.ylabel = "Signal (arb. units)" + options.xval_unit = "Hz" + return options + + def _generate_fit_guesses( + self, user_opt: curve.FitOptions + ) -> Union[curve.FitOptions, List[curve.FitOptions]]: + """Compute the initial guesses. + + Args: + user_opt: Fit options filled with user provided guess and bounds. + + Returns: + List of fit options that are passed to the fitter function. + """ + curve_data = self._data() + max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) + + user_opt.bounds.set_if_empty( + a=(-2 * max_abs_y, 2 * max_abs_y), + sigma=(0, np.ptp(curve_data.x)), + freq=(min(curve_data.x), max(curve_data.x)), + b=(-max_abs_y, max_abs_y), + ) + user_opt.p0.set_if_empty(b=curve.guess.constant_spectral_offset(curve_data.y)) + + y_ = curve_data.y - user_opt.p0["b"] + + _, peak_idx = curve.guess.max_height(y_, absolute=True) + fwhm = curve.guess.full_width_half_max(curve_data.x, y_, peak_idx) + + user_opt.p0.set_if_empty( + a=curve_data.y[peak_idx] - user_opt.p0["b"], + freq=curve_data.x[peak_idx], + sigma=fwhm / np.sqrt(8 * np.log(2)), + ) + + return user_opt + + def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: + """Algorithmic criteria for whether the fit is good or bad. + + A good fit has: + - a reduced chi-squared less than 3, + - a peak within the scanned frequency range, + - a standard deviation that is not larger than the scanned frequency range, + - a standard deviation that is wider than the smallest frequency increment, + - a signal-to-noise ratio, defined as the amplitude of the peak divided by the + square root of the median y-value less the fit offset, greater than a + threshold of two, and + - a standard error on the sigma of the Gaussian that is smaller than the sigma. + """ + curve_data = self._data() + + max_freq = np.max(curve_data.x) + min_freq = np.min(curve_data.x) + freq_increment = np.mean(np.diff(curve_data.x)) + + fit_a = fit_data.fitval("a").value + fit_b = fit_data.fitval("b").value + fit_freq = fit_data.fitval("freq").value + fit_sigma = fit_data.fitval("sigma").value + fit_sigma_err = fit_data.fitval("sigma").stderr + + snr = abs(fit_a) / np.sqrt(abs(np.median(curve_data.y) - fit_b)) + fit_width_ratio = fit_sigma / (max_freq - min_freq) + + criteria = [ + min_freq <= fit_freq <= max_freq, + 1.5 * freq_increment < fit_sigma, + fit_width_ratio < 0.25, + fit_data.reduced_chisq < 3, + (fit_sigma_err is None or fit_sigma_err < fit_sigma), + snr > 2, + ] + + if all(criteria): + return "good" + + return "bad" diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 0fa3390225..6f29a6ed4a 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -21,50 +21,49 @@ class ResonanceAnalysis(curve.CurveAnalysis): - r"""A class to analyze a resonance, typically seen as a peak. + r"""A class to analyze a resonance, typically seen as a Lorentzian peak. Overview - This analysis takes only single series. This series is fit by the Gaussian function. + This analysis takes only single series. This series is fit by the Lorentzian function. Fit Model - The fit is based on the following Gaussian function. + The fit is based on the following Lorentzian function. .. math:: - F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b + F(x) = \frac{a}{2\pi} \frac{\gamma}{(x - x0)^2 + (\gamma/2)^2} + b Fit Parameters - :math:`a`: Peak height. - :math:`b`: Base line. - - :math:`f`: Center frequency. This is the fit parameter of main interest. - - :math:`\sigma`: Standard deviation of Gaussian function. + - :math:`x0`: Center value. This is typically the fit parameter of interest. + - :math:`\gamma`: Linewidth. Initial Guesses - :math:`a`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - :math:`b`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.\ constant_spectral_offset`. - - :math:`f`: Frequency at max height position calculated by + - :math:`x0`: The max height position is calculated by the function :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - - :math:`\sigma`: Calculated from FWHM of peak :math:`w` - such that :math:`w / \sqrt{8} \ln{2}`, where FWHM is calculated by + - :math:`\gamma`: Calculated from FWHM of the peak using :func:`~qiskit_experiments.curve_analysis.guess.full_width_half_max`. Bounds - :math:`a`: [-2, 2] scaled with maximum signal value. - :math:`b`: [-1, 1] scaled with maximum signal value. - - :math:`f`: [min(x), max(x)] of frequency scan range. - - :math:`\sigma`: [0, :math:`\Delta x`] where :math:`\Delta x` - represents frequency scan range. + - :math:`f`: [min(x), max(x)] of x-value scan range. + - :math:`\gamma`: [0, :math:`\Delta x`] where :math:`\Delta x` + represents the x-value scan range. """ __series__ = [ curve.SeriesDef( - fit_func=lambda x, a, sigma, freq, b: curve.fit_function.gaussian( - x, amp=a, sigma=sigma, x0=freq, baseline=b + fit_func=lambda x, a, gamma, freq, b: curve.fit_function.lorentzian( + x, amp=a, gamma=gamma, x0=freq, baseline=b ), plot_color="blue", - model_description=r"a \exp(-(x-f)^2/(2\sigma^2)) + b", + model_description=r"a\gamma/(2\pi) / ((x - x0)^2 + (\gamma/2)^2) + b", ) ] @@ -94,7 +93,7 @@ def _generate_fit_guesses( user_opt.bounds.set_if_empty( a=(-2 * max_abs_y, 2 * max_abs_y), - sigma=(0, np.ptp(curve_data.x)), + gamma=(0, np.ptp(curve_data.x)), freq=(min(curve_data.x), max(curve_data.x)), b=(-max_abs_y, max_abs_y), ) @@ -108,7 +107,7 @@ def _generate_fit_guesses( user_opt.p0.set_if_empty( a=curve_data.y[peak_idx] - user_opt.p0["b"], freq=curve_data.x[peak_idx], - sigma=fwhm / np.sqrt(8 * np.log(2)), + gamma=fwhm, ) return user_opt @@ -124,7 +123,7 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: - a signal-to-noise ratio, defined as the amplitude of the peak divided by the square root of the median y-value less the fit offset, greater than a threshold of two, and - - a standard error on the sigma of the Gaussian that is smaller than the sigma. + - a standard error on the gamma of the Lorentzian that is smaller than the gamma. """ curve_data = self._data() @@ -135,18 +134,18 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: fit_a = fit_data.fitval("a").value fit_b = fit_data.fitval("b").value fit_freq = fit_data.fitval("freq").value - fit_sigma = fit_data.fitval("sigma").value - fit_sigma_err = fit_data.fitval("sigma").stderr + fit_gamma = fit_data.fitval("gamma").value + fit_gamma_err = fit_data.fitval("gamma").stderr snr = abs(fit_a) / np.sqrt(abs(np.median(curve_data.y) - fit_b)) - fit_width_ratio = fit_sigma / (max_freq - min_freq) + fit_width_ratio = fit_gamma / (max_freq - min_freq) criteria = [ min_freq <= fit_freq <= max_freq, - 1.5 * freq_increment < fit_sigma, + 1.5 * freq_increment < fit_gamma, fit_width_ratio < 0.25, fit_data.reduced_chisq < 3, - (fit_sigma_err is None or fit_sigma_err < fit_sigma), + (fit_gamma_err is None or fit_gamma_err < fit_gamma), snr > 2, ] From 8796b3254a63de252a87a79802bf83b834eafff4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:33:15 +0100 Subject: [PATCH 42/68] * Lorentz normalization, small bug fixes, black. --- qiskit_experiments/curve_analysis/fit_function.py | 6 +++--- .../curve_analysis/standard_analysis/resonance.py | 6 +++--- .../library/characterization/resonator_spectroscopy.py | 8 ++++---- test/test_resonator_spectroscopy.py | 4 +++- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/curve_analysis/fit_function.py b/qiskit_experiments/curve_analysis/fit_function.py index 8156296c3b..6f5ccedf19 100644 --- a/qiskit_experiments/curve_analysis/fit_function.py +++ b/qiskit_experiments/curve_analysis/fit_function.py @@ -79,13 +79,13 @@ def gaussian( def lorentzian( x: np.ndarray, amp: float = 1.0, gamma: float = 1.0, x0: float = 0.0, baseline: float = 0.0 -) -> np.ndarry: +) -> np.ndarray: r"""Lorentzian function .. math:: - y = \frac{\rm amp}{2\pi}\frac{\gamma}{(x - x0)^2 + (\gamma/2)^2} + {\rm baseline} + y = {\rm amp}\frac{(\gamma/2)^2}{(x - x0)^2 + (\gamma/2)^2} + {\rm baseline} """ - return amp * gamma / ((x - x0) ** 2 + (gamma / 2) ** 2) / (2 * np.pi) + baseline + return amp * (gamma / 2) ** 2 / ((x - x0) ** 2 + (gamma / 2) ** 2) + baseline def cos_decay( diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 6f29a6ed4a..0905e3f743 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -31,7 +31,7 @@ class ResonanceAnalysis(curve.CurveAnalysis): .. math:: - F(x) = \frac{a}{2\pi} \frac{\gamma}{(x - x0)^2 + (\gamma/2)^2} + b + F(x) = a \frac{(\gamma/2)^2}{(x - x0)^2 + (\gamma/2)^2} + b Fit Parameters - :math:`a`: Peak height. @@ -63,7 +63,7 @@ class ResonanceAnalysis(curve.CurveAnalysis): x, amp=a, gamma=gamma, x0=freq, baseline=b ), plot_color="blue", - model_description=r"a\gamma/(2\pi) / ((x - x0)^2 + (\gamma/2)^2) + b", + model_description=r"a (\gamma/2)^2 / ((x - x0)^2 + (\gamma/2)^2) + b", ) ] @@ -105,7 +105,7 @@ def _generate_fit_guesses( fwhm = curve.guess.full_width_half_max(curve_data.x, y_, peak_idx) user_opt.p0.set_if_empty( - a=curve_data.y[peak_idx] - user_opt.p0["b"], + a=(curve_data.y[peak_idx] - user_opt.p0["b"]), freq=curve_data.x[peak_idx], gamma=fwhm, ) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 8fcb515900..deed7aab0a 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -161,9 +161,7 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: if dt is None or granularity is None: raise QiskitError(f"{self.__class__.__name__} needs both dt and sample granularity.") - acq_dur = int( - granularity * (self.experiment_options.acquire_duration / dt // granularity) - ) + acq_dur = int(granularity * (self.experiment_options.acquire_duration / dt // granularity)) acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) duration = int(granularity * (self.experiment_options.duration / dt // granularity)) sigma = granularity * (self.experiment_options.sigma / dt // granularity) @@ -186,7 +184,9 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: ) with pulse.align_left(): - pulse.delay(acq_del, pulse.AcquireChannel(qubit)) + if acq_del != 0: + pulse.delay(acq_del, pulse.AcquireChannel(qubit)) + pulse.acquire(acq_dur, qubit, pulse.MemorySlot(0)) return schedule, freq_param diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 8b8f96e07e..17b92d0490 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -47,7 +47,9 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency delta_freq = freq_shift - self._freq_offset - return (0.5 * self._linewidth) ** 2 / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) + return (0.5 * self._linewidth ** 2 / np.pi) / ( + delta_freq ** 2 + (0.5 * self._linewidth) ** 2 + ) def _iq_phase(self, circuit: QuantumCircuit) -> float: """Add a phase to the IQ point depending on how far we are from the resonance. From 723202ce46ae3299a303468238511b2840770846 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:34:29 +0100 Subject: [PATCH 43/68] * Test normalozation. --- test/test_resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 17b92d0490..d315db11da 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -47,7 +47,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency delta_freq = freq_shift - self._freq_offset - return (0.5 * self._linewidth ** 2 / np.pi) / ( + return (0.5 * (self._linewidth / 2) ** 2 ) / ( delta_freq ** 2 + (0.5 * self._linewidth) ** 2 ) From dac16818c6d17af40e4320486919411d83fbc615 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:37:55 +0100 Subject: [PATCH 44/68] * Reno. --- .../notes/resonator-spectroscopy-89f790412838ba5b.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml index 28a6d34c62..9f08e43bf8 100644 --- a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml +++ b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml @@ -1,4 +1,9 @@ --- +fixes: + - | + The ResonanceAnalysis class has been switched from a Gaussian fit to a Lorentzian + fit function. Furthermore, the Gaussian fitting capability is preserved by moving + the Gaussian fitting to a new class called GaussianAnalysis. features: - | This change introduces the new experiment From ead7c55aaea85346b3f4b20e30ce8cfad0c9c42a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:40:44 +0100 Subject: [PATCH 45/68] * Removed center_frequency. --- .../library/characterization/spectroscopy.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 6d4c3dab31..867ba38ea2 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -109,21 +109,6 @@ def _backend_center_frequency(self) -> float: which depends on the nature of the spectroscopy experiment. """ - @property - def center_frequency(self) -> float: - """The center frequency of the spectroscopy experiment. - - Returns: - The center frequency in absolute terms. If the experiment is running in absolute mode - then we return the mid-point of the frequency range. However, if the experiment is - running in relative mode then we return the frequency of the backend with respect to - which the relative shift is applied. - """ - if self._absolute: - return (max(self._frequencies) - min(self._frequencies)) / 2 - else: - return self._backend_center_frequency - def _add_metadata(self, circuit: QuantumCircuit, freq: float, sched: pulse.ScheduleBlock): """Helper method to add the metadata to avoid code duplication with subclasses.""" From a994631a4c208e13082e58c1f99bbacf4ebe72f9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:45:17 +0100 Subject: [PATCH 46/68] * black. --- test/test_resonator_spectroscopy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index d315db11da..23d5a2399d 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -47,9 +47,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency delta_freq = freq_shift - self._freq_offset - return (0.5 * (self._linewidth / 2) ** 2 ) / ( - delta_freq ** 2 + (0.5 * self._linewidth) ** 2 - ) + return (0.5 * (self._linewidth / 2) ** 2) / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) def _iq_phase(self, circuit: QuantumCircuit) -> float: """Add a phase to the IQ point depending on how far we are from the resonance. From e7ccdf042dd40e3da404dd0154d7ebd3d07f9646 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 25 Jan 2022 15:57:39 +0100 Subject: [PATCH 47/68] * Qubit spec test align. --- test/test_qubit_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 1b7762c242..a382ddabf2 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -47,7 +47,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: """Returns the probability based on the frequency.""" 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)) + return (0.5 * (self._linewidth / 2) ** 2) / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) class TestQubitSpectroscopy(QiskitExperimentsTestCase): From 9dbe059389227bd3b2a39c2292c8d9acc65ad711 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 26 Jan 2022 13:48:07 +0100 Subject: [PATCH 48/68] * Device components. --- qiskit_experiments/framework/base_analysis.py | 19 ++++++++++++------- .../resonator_spectroscopy_analysis.py | 5 +++++ test/test_qubit_spectroscopy.py | 1 + test/test_resonator_spectroscopy.py | 1 + 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/framework/base_analysis.py b/qiskit_experiments/framework/base_analysis.py index 37f898f25b..6e3cf0fe1a 100644 --- a/qiskit_experiments/framework/base_analysis.py +++ b/qiskit_experiments/framework/base_analysis.py @@ -144,13 +144,7 @@ def run( if not replace_results and _requires_copy(experiment_data): experiment_data = experiment_data.copy() - # Get experiment device components - if "physical_qubits" in experiment_data.metadata: - experiment_components = [ - Qubit(qubit) for qubit in experiment_data.metadata["physical_qubits"] - ] - else: - experiment_components = [] + experiment_components = self._get_experiment_components(experiment_data) # Set Analysis options if not options: @@ -179,6 +173,17 @@ def run_analysis(expdata): return experiment_data + def _get_experiment_components(self, experiment_data: ExperimentData): + """Subclasses may override this method to specify the experiment components.""" + if "physical_qubits" in experiment_data.metadata: + experiment_components = [ + Qubit(qubit) for qubit in experiment_data.metadata["physical_qubits"] + ] + else: + experiment_components = [] + + return experiment_components + def _format_analysis_result(self, data, experiment_id, experiment_components=None): """Format run analysis result to DbAnalysisResult""" device_components = [] diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index e8ff74a639..251089701e 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -20,6 +20,7 @@ from qiskit_experiments.framework import AnalysisResultData, ExperimentData from qiskit_experiments.framework.matplotlib import get_non_gui_ax from qiskit_experiments.data_processing.nodes import ProjectorType +from qiskit_experiments.database_service.device_component import Resonator class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): @@ -33,6 +34,10 @@ def _default_options(cls): options.plot_iq_data = True return options + def _get_experiment_components(self, experiment_data: ExperimentData): + """Return resonators as experiment components.""" + return [Resonator(qubit) for qubit in experiment_data.metadata["physical_qubits"]] + def _run_analysis( self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]: diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index a382ddabf2..2a57445453 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -69,6 +69,7 @@ def test_spectroscopy_end2end_classified(self): self.assertTrue(4.999e9 < value < 5.001e9) self.assertEqual(result.quality, "good") + self.assertEqual(str(result.device_components[0]), f"Q{qubit}") # Test if we find still find the peak when it is shifted by 5 MHz. backend = SpectroscopyBackend(line_width=2e6, freq_offset=5.0e6) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 23d5a2399d..3db7baca43 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -80,6 +80,7 @@ def test_end_to_end(self): value = result.value.value self.assertTrue(6.6099e9 < value < 6.6101e9) + self.assertEqual(str(result.device_components[0]), f"R{qubit}") def test_experiment_config(self): """Test converting to and from config works""" From 3e258cb9f2a47e8e12ccc17245b42f9657e78ddb Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 26 Jan 2022 13:52:43 +0100 Subject: [PATCH 49/68] * Resonator kappa in result. --- .../analysis/resonator_spectroscopy_analysis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 251089701e..2459d724f6 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -30,7 +30,10 @@ class ResonatorSpectroscopyAnalysis(ResonanceAnalysis): def _default_options(cls): options = super()._default_options() options.dimensionality_reduction = ProjectorType.ABS - options.result_parameters = [curve.ParameterRepr("freq", "meas_freq", "Hz")] + options.result_parameters = [ + curve.ParameterRepr("freq", "meas_freq", "Hz"), + curve.ParameterRepr("gamma", "kappa", "Hz"), + ] options.plot_iq_data = True return options From fd979885e982ee06467709179acb41fb17e03766 Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Wed, 26 Jan 2022 17:06:02 +0100 Subject: [PATCH 50/68] Update qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py --- .../analysis/resonator_spectroscopy_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 2459d724f6..9f71ee4ab8 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -31,7 +31,7 @@ def _default_options(cls): options = super()._default_options() options.dimensionality_reduction = ProjectorType.ABS options.result_parameters = [ - curve.ParameterRepr("freq", "meas_freq", "Hz"), + curve.ParameterRepr("freq", "res_freq0", "Hz"), curve.ParameterRepr("gamma", "kappa", "Hz"), ] options.plot_iq_data = True From 5d9f0b437d89f670eb23059d5e13a300eba6879c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 26 Jan 2022 19:23:36 +0100 Subject: [PATCH 51/68] * Resonator spectroscopy kappa and fit function. --- .../curve_analysis/fit_function.py | 8 ++--- .../standard_analysis/resonance.py | 30 +++++++++---------- .../resonator_spectroscopy_analysis.py | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/qiskit_experiments/curve_analysis/fit_function.py b/qiskit_experiments/curve_analysis/fit_function.py index 6f5ccedf19..8afc1d22ff 100644 --- a/qiskit_experiments/curve_analysis/fit_function.py +++ b/qiskit_experiments/curve_analysis/fit_function.py @@ -78,14 +78,14 @@ def gaussian( def lorentzian( - x: np.ndarray, amp: float = 1.0, gamma: float = 1.0, x0: float = 0.0, baseline: float = 0.0 + x: np.ndarray, amp: float = 1.0, kappa: float = 1.0, x0: float = 0.0, baseline: float = 0.0 ) -> np.ndarray: - r"""Lorentzian function + r"""Lorentzian function for spectroscopy .. math:: - y = {\rm amp}\frac{(\gamma/2)^2}{(x - x0)^2 + (\gamma/2)^2} + {\rm baseline} + y = {\rm amp}{\rm abs}\left(\frac{1}{1 + 2i(x - x0)/\kappa}\right) + {\rm baseline} """ - return amp * (gamma / 2) ** 2 / ((x - x0) ** 2 + (gamma / 2) ** 2) + baseline + return amp * np.abs(1 / (1 + 2.0j * (x - x0) / kappa)) + baseline def cos_decay( diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 0905e3f743..5b95ce512b 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -31,13 +31,13 @@ class ResonanceAnalysis(curve.CurveAnalysis): .. math:: - F(x) = a \frac{(\gamma/2)^2}{(x - x0)^2 + (\gamma/2)^2} + b + F(x) = a{\rm abs}\left(\frac{1}{1 + 2i(x - x0)/\kappa}\right) + b Fit Parameters - :math:`a`: Peak height. - :math:`b`: Base line. - :math:`x0`: Center value. This is typically the fit parameter of interest. - - :math:`\gamma`: Linewidth. + - :math:`\kappa`: Linewidth. Initial Guesses - :math:`a`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. @@ -45,25 +45,25 @@ class ResonanceAnalysis(curve.CurveAnalysis): constant_spectral_offset`. - :math:`x0`: The max height position is calculated by the function :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - - :math:`\gamma`: Calculated from FWHM of the peak using + - :math:`\kappa`: Calculated from FWHM of the peak using :func:`~qiskit_experiments.curve_analysis.guess.full_width_half_max`. Bounds - :math:`a`: [-2, 2] scaled with maximum signal value. - :math:`b`: [-1, 1] scaled with maximum signal value. - :math:`f`: [min(x), max(x)] of x-value scan range. - - :math:`\gamma`: [0, :math:`\Delta x`] where :math:`\Delta x` + - :math:`\kappa`: [0, :math:`\Delta x`] where :math:`\Delta x` represents the x-value scan range. """ __series__ = [ curve.SeriesDef( - fit_func=lambda x, a, gamma, freq, b: curve.fit_function.lorentzian( - x, amp=a, gamma=gamma, x0=freq, baseline=b + fit_func=lambda x, a, kappa, freq, b: curve.fit_function.lorentzian( + x, amp=a, kappa=kappa, x0=freq, baseline=b ), plot_color="blue", - model_description=r"a (\gamma/2)^2 / ((x - x0)^2 + (\gamma/2)^2) + b", + model_description=r"a (\kappa/2)^2 / ((x - x0)^2 + (\kappa/2)^2) + b", ) ] @@ -93,7 +93,7 @@ def _generate_fit_guesses( user_opt.bounds.set_if_empty( a=(-2 * max_abs_y, 2 * max_abs_y), - gamma=(0, np.ptp(curve_data.x)), + kappa=(0, np.ptp(curve_data.x)), freq=(min(curve_data.x), max(curve_data.x)), b=(-max_abs_y, max_abs_y), ) @@ -107,7 +107,7 @@ def _generate_fit_guesses( user_opt.p0.set_if_empty( a=(curve_data.y[peak_idx] - user_opt.p0["b"]), freq=curve_data.x[peak_idx], - gamma=fwhm, + kappa=fwhm, ) return user_opt @@ -123,7 +123,7 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: - a signal-to-noise ratio, defined as the amplitude of the peak divided by the square root of the median y-value less the fit offset, greater than a threshold of two, and - - a standard error on the gamma of the Lorentzian that is smaller than the gamma. + - a standard error on the kappa of the Lorentzian that is smaller than the kappa. """ curve_data = self._data() @@ -134,18 +134,18 @@ def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: fit_a = fit_data.fitval("a").value fit_b = fit_data.fitval("b").value fit_freq = fit_data.fitval("freq").value - fit_gamma = fit_data.fitval("gamma").value - fit_gamma_err = fit_data.fitval("gamma").stderr + fit_kappa = fit_data.fitval("kappa").value + fit_kappa_err = fit_data.fitval("kappa").stderr snr = abs(fit_a) / np.sqrt(abs(np.median(curve_data.y) - fit_b)) - fit_width_ratio = fit_gamma / (max_freq - min_freq) + fit_width_ratio = fit_kappa / (max_freq - min_freq) criteria = [ min_freq <= fit_freq <= max_freq, - 1.5 * freq_increment < fit_gamma, + 1.5 * freq_increment < fit_kappa, fit_width_ratio < 0.25, fit_data.reduced_chisq < 3, - (fit_gamma_err is None or fit_gamma_err < fit_gamma), + (fit_kappa_err is None or fit_kappa_err < fit_kappa), snr > 2, ] diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 9f71ee4ab8..5dcb4b2ab7 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -32,7 +32,7 @@ def _default_options(cls): options.dimensionality_reduction = ProjectorType.ABS options.result_parameters = [ curve.ParameterRepr("freq", "res_freq0", "Hz"), - curve.ParameterRepr("gamma", "kappa", "Hz"), + curve.ParameterRepr("kappa", "kappa", "Hz"), ] options.plot_iq_data = True return options From 4b7715dc407347cf35577bf0cf71c6b9d444b0d3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 27 Jan 2022 12:29:08 +0100 Subject: [PATCH 52/68] * Adapted tests. --- .../curve_analysis/standard_analysis/resonance.py | 2 +- test/test_qubit_spectroscopy.py | 9 ++++++--- test/test_resonator_spectroscopy.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 5b95ce512b..24687d328f 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -63,7 +63,7 @@ class ResonanceAnalysis(curve.CurveAnalysis): x, amp=a, kappa=kappa, x0=freq, baseline=b ), plot_color="blue", - model_description=r"a (\kappa/2)^2 / ((x - x0)^2 + (\kappa/2)^2) + b", + model_description=r"a abs(1 / (1 + 2i * (x - x_0) / \kappa)) + b", ) ] diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 2a57445453..479383b1fc 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -47,7 +47,8 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: """Returns the probability based on the frequency.""" freq_shift = next(iter(circuit.calibrations["Spec"]))[1][0] delta_freq = freq_shift - self._freq_offset - return (0.5 * (self._linewidth / 2) ** 2) / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) + + return np.abs(1 / (1 + 2.0j * delta_freq / self._linewidth)) class TestQubitSpectroscopy(QiskitExperimentsTestCase): @@ -86,7 +87,7 @@ def test_spectroscopy_end2end_classified(self): def test_spectroscopy_end2end_kerneled(self): """End to end test of the spectroscopy experiment on IQ data.""" - backend = SpectroscopyBackend(line_width=2e6) + backend = SpectroscopyBackend(line_width=2e6, iq_cluster_centers=(-1, -1, 1, 1)) qubit = 0 freq01 = backend.defaults().qubit_freq_est[qubit] frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) @@ -100,7 +101,9 @@ def test_spectroscopy_end2end_kerneled(self): self.assertEqual(result.quality, "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) + backend = SpectroscopyBackend( + line_width=2e6, freq_offset=5.0e6, iq_cluster_centers=(-1, -1, 1, 1) + ) spec = QubitSpectroscopy(qubit, frequencies) expdata = spec.run(backend) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 3db7baca43..a7fcc1e1b3 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -47,7 +47,7 @@ def _compute_probability(self, circuit: QuantumCircuit) -> float: freq_shift = next(iter(circuit.calibrations["measure"].values())).blocks[0].frequency delta_freq = freq_shift - self._freq_offset - return (0.5 * (self._linewidth / 2) ** 2) / (delta_freq ** 2 + (0.5 * self._linewidth) ** 2) + return np.abs(1 / (1 + 2.0j * delta_freq / self._linewidth)) def _iq_phase(self, circuit: QuantumCircuit) -> float: """Add a phase to the IQ point depending on how far we are from the resonance. From 73eaa4e3664dcaaf3c11d551a84c40e093d27a0f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 27 Jan 2022 12:59:42 +0100 Subject: [PATCH 53/68] * Update default options. --- .../library/characterization/resonator_spectroscopy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index deed7aab0a..fbcfa71bf2 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -77,10 +77,11 @@ def _default_experiment_options(cls) -> Options: options = super()._default_experiment_options() options.amp = 1 - options.acquire_duration = 240e-9 + options.acquire_duration = 480e-9 options.acquire_delay = 0 - options.duration = 240e-9 + options.duration = 480e-9 options.sigma = 60e-9 + options.width = 360e-9 return options From 8eb9d202187e1a80540c06dc362ccb0caa61af03 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 27 Jan 2022 18:06:29 +0100 Subject: [PATCH 54/68] * Moved qubit spectroscopy to units of seconds. --- .../characterization/qubit_spectroscopy.py | 13 +++++++++--- .../resonator_spectroscopy.py | 7 +------ .../library/characterization/spectroscopy.py | 20 +++++++++++++++---- test/test_qubit_spectroscopy.py | 6 ++---- test/test_resonator_spectroscopy.py | 2 -- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index 49bd8414c2..a78727816a 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -73,14 +73,21 @@ def _template_circuit(self, freq_param) -> QuantumCircuit: def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" freq_param = Parameter("frequency") + + dt, granularity = self._get_dt_and_granularity() + + duration = int(granularity * (self.experiment_options.duration / dt // granularity)) + sigma = granularity * (self.experiment_options.sigma / dt // granularity) + width = granularity * (self.experiment_options.width / dt // granularity) + with pulse.build(backend=self.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, + duration=duration, amp=self.experiment_options.amp, - sigma=self.experiment_options.sigma, - width=self.experiment_options.width, + sigma=sigma, + width=width, ), pulse.DriveChannel(self.physical_qubits[0]), ) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index fbcfa71bf2..c15ae66a72 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -155,12 +155,7 @@ def _template_circuit(self) -> QuantumCircuit: def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" - dt = getattr(self.backend.configuration(), "dt", None) - constraints = getattr(self.backend.configuration(), "timing_constraints", {}) - granularity = constraints.get("granularity", None) - - if dt is None or granularity is None: - raise QiskitError(f"{self.__class__.__name__} needs both dt and sample granularity.") + dt, granularity = self._get_dt_and_granularity() acq_dur = int(granularity * (self.experiment_options.acquire_duration / dt // granularity)) acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index 867ba38ea2..fbd8796ab4 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -13,7 +13,7 @@ """Abstract spectroscopy experiment base class.""" from abc import ABC, abstractmethod -from typing import Iterable, Optional +from typing import Iterable, Optional, Tuple import numpy as np import qiskit.pulse as pulse @@ -45,8 +45,8 @@ def _default_experiment_options(cls) -> Options: options = super()._default_experiment_options() options.amp = 0.1 - options.duration = 1024 - options.sigma = 256 + options.duration = 240e-9 + options.sigma = 60e-9 options.width = 0 return options @@ -57,7 +57,7 @@ def _default_run_options(cls) -> Options: options = super()._default_run_options() options.meas_level = MeasLevel.KERNELED - options.meas_return = "single" + options.meas_return = "avg" return options @@ -97,6 +97,18 @@ def __init__( self.set_experiment_options(**experiment_options) + def _get_dt_and_granularity(self) -> Tuple[float, float]: + """Extract the time per sample and granularity from the backend.""" + + dt = getattr(self.backend.configuration(), "dt", None) + constraints = getattr(self.backend.configuration(), "timing_constraints", {}) + granularity = constraints.get("granularity", None) + + if dt is None or granularity is None: + raise QiskitError(f"{self.__class__.__name__} needs both dt and sample granularity.") + + return dt, granularity + @property @abstractmethod def _backend_center_frequency(self) -> float: diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 479383b1fc..5bd4fa5ebd 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -36,13 +36,11 @@ def __init__( super().__init__(iq_cluster_centers, iq_cluster_width) - self.configuration().basis_gates = ["x"] - + self._configuration.basis_gates = ["x"] + self._configuration.timing_constraints = {"granularity": 16} self._linewidth = line_width self._freq_offset = freq_offset - super().__init__(iq_cluster_centers, iq_cluster_width) - def _compute_probability(self, circuit: QuantumCircuit) -> float: """Returns the probability based on the frequency.""" freq_shift = next(iter(circuit.calibrations["Spec"]))[1][0] diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index a7fcc1e1b3..2ce539f928 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -38,8 +38,6 @@ def __init__( self._linewidth = line_width self._freq_offset = freq_offset - - super().__init__(iq_cluster_centers, iq_cluster_width) self._configuration.timing_constraints = {"granularity": 16} def _compute_probability(self, circuit: QuantumCircuit) -> float: From 9a6417fb6c037dd2d012621c626438645ece88d9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 27 Jan 2022 19:16:56 +0100 Subject: [PATCH 55/68] * set backend --- .../library/characterization/qubit_spectroscopy.py | 2 +- .../characterization/resonator_spectroscopy.py | 2 +- .../library/characterization/spectroscopy.py | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index a78727816a..e9897b428d 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -74,7 +74,7 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" freq_param = Parameter("frequency") - dt, granularity = self._get_dt_and_granularity() + dt, granularity = self._dt, self._granularity duration = int(granularity * (self.experiment_options.duration / dt // granularity)) sigma = granularity * (self.experiment_options.sigma / dt // granularity) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index c15ae66a72..a2a5e5199c 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -155,7 +155,7 @@ def _template_circuit(self) -> QuantumCircuit: def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: """Create the spectroscopy schedule.""" - dt, granularity = self._get_dt_and_granularity() + dt, granularity = self._dt, self._granularity acq_dur = int(granularity * (self.experiment_options.acquire_duration / dt // granularity)) acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index fbd8796ab4..f89c92f257 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -97,18 +97,17 @@ def __init__( self.set_experiment_options(**experiment_options) - def _get_dt_and_granularity(self) -> Tuple[float, float]: - """Extract the time per sample and granularity from the backend.""" + def _set_backend(self, backend: Backend): + """Set the backend for the experiment and extract config information.""" + super()._set_backend(backend) - dt = getattr(self.backend.configuration(), "dt", None) + self._dt = getattr(self.backend.configuration(), "dt", None) constraints = getattr(self.backend.configuration(), "timing_constraints", {}) - granularity = constraints.get("granularity", None) + self._granularity = constraints.get("granularity", None) - if dt is None or granularity is None: + if self._dt is None or self._granularity is None: raise QiskitError(f"{self.__class__.__name__} needs both dt and sample granularity.") - return dt, granularity - @property @abstractmethod def _backend_center_frequency(self) -> float: From a9997449aad11cacb9cdde12f4a18221ccd78bb2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 27 Jan 2022 20:15:56 +0100 Subject: [PATCH 56/68] * Lint --- qiskit_experiments/library/characterization/spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py index f89c92f257..afc8dd3ebe 100644 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ b/qiskit_experiments/library/characterization/spectroscopy.py @@ -13,7 +13,7 @@ """Abstract spectroscopy experiment base class.""" from abc import ABC, abstractmethod -from typing import Iterable, Optional, Tuple +from typing import Iterable, Optional import numpy as np import qiskit.pulse as pulse From fac4995d498cd300ad28237c5a8c82870b8bbe06 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 28 Jan 2022 18:17:40 +0100 Subject: [PATCH 57/68] * Square root lorentzian. --- qiskit_experiments/curve_analysis/fit_function.py | 4 ++-- .../curve_analysis/standard_analysis/resonance.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/curve_analysis/fit_function.py b/qiskit_experiments/curve_analysis/fit_function.py index 8afc1d22ff..ede60107e7 100644 --- a/qiskit_experiments/curve_analysis/fit_function.py +++ b/qiskit_experiments/curve_analysis/fit_function.py @@ -77,10 +77,10 @@ def gaussian( return amp * np.exp(-((x - x0) ** 2) / (2 * sigma ** 2)) + baseline -def lorentzian( +def sqrt_lorentzian( x: np.ndarray, amp: float = 1.0, kappa: float = 1.0, x0: float = 0.0, baseline: float = 0.0 ) -> np.ndarray: - r"""Lorentzian function for spectroscopy + r"""Square-root Lorentzian function for spectroscopy. .. math:: y = {\rm amp}{\rm abs}\left(\frac{1}{1 + 2i(x - x0)/\kappa}\right) + {\rm baseline} diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py index 24687d328f..e6997657b0 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py @@ -21,10 +21,11 @@ class ResonanceAnalysis(curve.CurveAnalysis): - r"""A class to analyze a resonance, typically seen as a Lorentzian peak. + r"""A class to analyze a resonance peak with a square rooted Lorentzian function. Overview - This analysis takes only single series. This series is fit by the Lorentzian function. + This analysis takes only single series. This series is fit to the square root of + a Lorentzian function. Fit Model The fit is based on the following Lorentzian function. @@ -59,7 +60,7 @@ class ResonanceAnalysis(curve.CurveAnalysis): __series__ = [ curve.SeriesDef( - fit_func=lambda x, a, kappa, freq, b: curve.fit_function.lorentzian( + fit_func=lambda x, a, kappa, freq, b: curve.fit_function.sqrt_lorentzian( x, amp=a, kappa=kappa, x0=freq, baseline=b ), plot_color="blue", From 9358a2b4ebce314f36654bd741dab9d8724e4e4e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 28 Jan 2022 18:30:38 +0100 Subject: [PATCH 58/68] * Remove acquire delay and duration. --- .../characterization/resonator_spectroscopy.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index a2a5e5199c..0b26c6510f 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -65,10 +65,6 @@ def _default_experiment_options(cls) -> Options: Experiment Options: amp (float): The amplitude of the spectroscopy pulse. Defaults to 1 and must be between 0 and 1. - acquire_duration (float): The duration of the acquisition instruction. By - default is lasts 240 ns, i.e. the same duration as the measurement pulse. - acquire_delay (float): The duration by which to delay the acquire instruction - with respect to the measurement pulse. duration (float): The duration in seconds of the spectroscopy pulse. sigma (float): The standard deviation of the spectroscopy pulse in seconds. width (float): The width of the flat-top part of the GaussianSquare pulse in @@ -77,8 +73,6 @@ def _default_experiment_options(cls) -> Options: options = super()._default_experiment_options() options.amp = 1 - options.acquire_duration = 480e-9 - options.acquire_delay = 0 options.duration = 480e-9 options.sigma = 60e-9 options.width = 360e-9 @@ -157,8 +151,6 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: dt, granularity = self._dt, self._granularity - acq_dur = int(granularity * (self.experiment_options.acquire_duration / dt // granularity)) - acq_del = int(granularity * (self.experiment_options.acquire_delay / dt // granularity)) duration = int(granularity * (self.experiment_options.duration / dt // granularity)) sigma = granularity * (self.experiment_options.sigma / dt // granularity) width = granularity * (self.experiment_options.width / dt // granularity) @@ -178,12 +170,7 @@ def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: ), pulse.MeasureChannel(qubit), ) - - with pulse.align_left(): - if acq_del != 0: - pulse.delay(acq_del, pulse.AcquireChannel(qubit)) - - pulse.acquire(acq_dur, qubit, pulse.MemorySlot(0)) + pulse.acquire(duration, qubit, pulse.MemorySlot(0)) return schedule, freq_param From da67d698b2bd43a19245bfb98a1c3713bd1590ed Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 28 Jan 2022 18:35:28 +0100 Subject: [PATCH 59/68] * Doc fix. --- qiskit_experiments/curve_analysis/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/curve_analysis/__init__.py b/qiskit_experiments/curve_analysis/__init__.py index 2272883672..1091596bf9 100644 --- a/qiskit_experiments/curve_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/__init__.py @@ -74,7 +74,7 @@ fit_function.cos_decay fit_function.exponential_decay fit_function.gaussian - fit_function.lorentzian + fit_function.sqrt_lorentzian fit_function.sin fit_function.sin_decay fit_function.bloch_oscillation_x From 35e5af4217101d336eee5eed26873126f4e7a913 Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Fri, 28 Jan 2022 20:28:47 +0100 Subject: [PATCH 60/68] Update qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py Co-authored-by: Will Shanks --- .../analysis/resonator_spectroscopy_analysis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py index 5dcb4b2ab7..09d826b59c 100644 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py @@ -62,9 +62,11 @@ def _run_analysis( if len(mem.shape) == 3: for idx in range(mem.shape[1]): iqs.append(np.average(mem[:, idx, :], axis=0)) + else: + iqs.append(mem) if len(iqs) > 0: - iqs = np.array(iqs) + iqs = np.vstack(iqs) axis.scatter(iqs[:, 0], iqs[:, 1], color="b") axis.set_xlabel( "In phase [arb. units]", fontsize=self.options.style.axis_label_size From 45f6dac2b3ee9bae17db645ad38dcefe5d8b37eb Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Wed, 2 Feb 2022 12:04:19 +0100 Subject: [PATCH 61/68] Update test/test_resonator_spectroscopy.py Co-authored-by: Naoki Kanazawa --- test/test_resonator_spectroscopy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 2ce539f928..1decdaef42 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -78,6 +78,7 @@ def test_end_to_end(self): value = result.value.value self.assertTrue(6.6099e9 < value < 6.6101e9) + self.assertAlmostEqual(value, res_freq, delta=0.1e6) self.assertEqual(str(result.device_components[0]), f"R{qubit}") def test_experiment_config(self): From 83a1b3e80aa05786805f77a216a5251a00247961 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 11:50:04 +0100 Subject: [PATCH 62/68] * Docs --- .../characterization/resonator_spectroscopy.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 0b26c6510f..7b526ff510 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -49,6 +49,20 @@ class ResonatorSpectroscopy(Spectroscopy): meaningful signal. The default data processing workflow will therefore reduce the two- dimensional IQ data to one-dimensional data using the magnitude of each IQ point. + # section: examples + + The resonator spectroscopy experiment can be run by doing: + + .. code:: python + + qubit = 1 + spec = ResonatorSpectroscopy(qubit, backend) + exp_data = spec.run().block_for_results() + exp_data.figure(0) + + This will measure the resonator attached to qubit 1 and report the resonance frequency + as well as the kappa, i.e. the line width, of the resonator. + # section: analysis_ref :py:class:`ResonatorSpectroscopyAnalysis` From 999073634a6d423962e4d87a25a2d11562c7d329 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 12:00:21 +0100 Subject: [PATCH 63/68] * Reno --- .../notes/resonator-spectroscopy-89f790412838ba5b.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml index 9f08e43bf8..63a9c378be 100644 --- a/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml +++ b/releasenotes/notes/resonator-spectroscopy-89f790412838ba5b.yaml @@ -3,7 +3,15 @@ fixes: - | The ResonanceAnalysis class has been switched from a Gaussian fit to a Lorentzian fit function. Furthermore, the Gaussian fitting capability is preserved by moving - the Gaussian fitting to a new class called GaussianAnalysis. + the Gaussian fitting to a new class called GaussianAnalysis. Note that the + previous analysis can be used by doing: + + .. code:: python + + spec = ResonatorSpectroscopy(qubit, backend) + spec.analysis = GaussianAnalysis() + + where :code:`GaussianAnalysis` is imported from ``curve_analysis``. features: - | This change introduces the new experiment From 77e8fb4e6b0b70ef00629c5de07ac5a3ec2a6992 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 12:48:08 +0100 Subject: [PATCH 64/68] * Added more tests values. --- test/test_resonator_spectroscopy.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_resonator_spectroscopy.py b/test/test_resonator_spectroscopy.py index 1decdaef42..a4326f144d 100644 --- a/test/test_resonator_spectroscopy.py +++ b/test/test_resonator_spectroscopy.py @@ -15,6 +15,7 @@ from test.base import QiskitExperimentsTestCase from typing import Tuple import numpy as np +from ddt import ddt, data from qiskit import QuantumCircuit @@ -60,14 +61,16 @@ def _iq_phase(self, circuit: QuantumCircuit) -> float: return delta_freq / self._linewidth +@ddt class TestResonatorSpectroscopy(QiskitExperimentsTestCase): """Tests for the resonator spectroscopy experiment.""" - def test_end_to_end(self): + @data(-5e6, -2e6, 0, 1e6, 3e6) + def test_end_to_end(self, freq_shift): """Test the experiment from end to end.""" qubit = 1 - backend = ResonatorSpectroscopyBackend(freq_offset=10e6) + backend = ResonatorSpectroscopyBackend(freq_offset=freq_shift) res_freq = backend.defaults().meas_freq_est[qubit] frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 51) @@ -77,8 +80,7 @@ def test_end_to_end(self): result = expdata.analysis_results(1) value = result.value.value - self.assertTrue(6.6099e9 < value < 6.6101e9) - self.assertAlmostEqual(value, res_freq, delta=0.1e6) + self.assertAlmostEqual(value, res_freq + freq_shift, delta=0.1e6) self.assertEqual(str(result.device_components[0]), f"R{qubit}") def test_experiment_config(self): From 322ed86662d53b5f194ee987443e7c296de62353 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 12:50:18 +0100 Subject: [PATCH 65/68] * Black --- test/data_processing/test_nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 237e0bd768..44a80295a3 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -152,8 +152,8 @@ def test_simple(self): processed = ToAbs()(np.array(data)) - val = np.sqrt(2 ** 2 + 3 ** 2) - val_err = np.sqrt(2 ** 2 * 0.2 ** 2 + 2 ** 2 * 0.3 ** 2) / val + val = np.sqrt(2**2 + 3**2) + val_err = np.sqrt(2**2 * 0.2**2 + 2**2 * 0.3**2) / val expected = np.array( [ From ba5d9ac64f73675b1907d9a5e99a8601be3d5a3d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 13:06:43 +0100 Subject: [PATCH 66/68] * Docs. --- .../library/characterization/resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 7b526ff510..936e10c341 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -49,7 +49,7 @@ class ResonatorSpectroscopy(Spectroscopy): meaningful signal. The default data processing workflow will therefore reduce the two- dimensional IQ data to one-dimensional data using the magnitude of each IQ point. - # section: examples + # section: example The resonator spectroscopy experiment can be run by doing: From 0664a21c7f6c0395d70d837d9579d833cea7e97f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 3 Feb 2022 18:45:10 +0100 Subject: [PATCH 67/68] * Added warning in resonator spectroscopy docstring. --- .../library/characterization/resonator_spectroscopy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 936e10c341..7fd9b9e27c 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -49,6 +49,11 @@ class ResonatorSpectroscopy(Spectroscopy): meaningful signal. The default data processing workflow will therefore reduce the two- dimensional IQ data to one-dimensional data using the magnitude of each IQ point. + .. warning:: + Some backends may not have the required functionality to properly support resonator + spectroscopy experiments. The experiment may not work or the resulting resonance + may not properly reflect the properties of the readout resonator. + # section: example The resonator spectroscopy experiment can be run by doing: From 2904da96c7a53c1c5e431d735c42711d2b10636a Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Fri, 4 Feb 2022 08:53:10 +0100 Subject: [PATCH 68/68] Update qiskit_experiments/library/characterization/resonator_spectroscopy.py Co-authored-by: Naoki Kanazawa --- .../library/characterization/resonator_spectroscopy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py index 7fd9b9e27c..df4ef21822 100644 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ b/qiskit_experiments/library/characterization/resonator_spectroscopy.py @@ -49,7 +49,7 @@ class ResonatorSpectroscopy(Spectroscopy): meaningful signal. The default data processing workflow will therefore reduce the two- dimensional IQ data to one-dimensional data using the magnitude of each IQ point. - .. warning:: + # section: warning Some backends may not have the required functionality to properly support resonator spectroscopy experiments. The experiment may not work or the resulting resonance may not properly reflect the properties of the readout resonator.