From 0bb8b86e2d4076d61a7b75b19add5f5efda3cc6a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 8 Jun 2021 14:14:03 +0200 Subject: [PATCH] * Added ResonatorSpectroscopy and its analysis class. * Added ToAbs IQ data processing node. * Extended functionality of get_to_signal_processor. --- .../characterization/qubit_spectroscopy.py | 8 +- .../resonator_spectroscopy.py | 110 ++++++++++++++++++ qiskit_experiments/data_processing/nodes.py | 31 +++++ .../data_processing/processor_library.py | 19 ++- 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 qiskit_experiments/characterization/resonator_spectroscopy.py diff --git a/qiskit_experiments/characterization/qubit_spectroscopy.py b/qiskit_experiments/characterization/qubit_spectroscopy.py index 5aaf3f2480..5d791335b5 100644 --- a/qiskit_experiments/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/characterization/qubit_spectroscopy.py @@ -81,6 +81,7 @@ def _default_options(cls): sigma_bounds=None, freq_bounds=None, offset_bounds=(-2, 2), + dimensionality_reduction="SVD", ) # pylint: disable=arguments-differ, unused-argument @@ -98,6 +99,7 @@ def _run_analysis( offset_bounds: Tuple[float, float] = (-2, 2), plot: bool = True, ax: Optional["AxesSubplot"] = None, + dimensionality_reduction: str = "SVD", **kwargs, ) -> Tuple[AnalysisResult, None]: """Analyze the given data by fitting it to a Gaussian. @@ -145,7 +147,11 @@ def _run_analysis( # Pick a data processor. if data_processor is None: - data_processor = get_to_signal_processor(meas_level=meas_level, meas_return=meas_return) + data_processor = get_to_signal_processor( + meas_level=meas_level, + meas_return=meas_return, + dimensionality_reduction=dimensionality_reduction + ) data_processor.train(experiment_data.data()) y_sigmas = np.array([data_processor(datum) for datum in experiment_data.data()]) diff --git a/qiskit_experiments/characterization/resonator_spectroscopy.py b/qiskit_experiments/characterization/resonator_spectroscopy.py new file mode 100644 index 0000000000..edb6618b9f --- /dev/null +++ b/qiskit_experiments/characterization/resonator_spectroscopy.py @@ -0,0 +1,110 @@ +# 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 + +from qiskit import QuantumCircuit +from qiskit.circuit import Gate, Parameter +from qiskit.exceptions import QiskitError +from qiskit.providers import Backend +import qiskit.pulse as pulse +from qiskit.providers.options import Options +from qiskit.pulse.transforms import block_to_schedule + +from qiskit_experiments.characterization.qubit_spectroscopy import QubitSpectroscopy, SpectroscopyAnalysis + + +class ResonatorSpectroscopyAnalysis(SpectroscopyAnalysis): + """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.""" + + __analysis_class__ = ResonatorSpectroscopyAnalysis + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default option values used for the spectroscopy pulse.""" + return Options( + amp=0.1, + duration=2688, + sigma=64, + width=2432, + ) + + def circuits(self, backend: Optional[Backend] = None): + """Create the circuit for the spectroscopy experiment. + + The circuits are based on a GaussianSquare pulse and a frequency_shift instruction + encapsulated in a gate. + + Args: + backend: A backend object. + + Returns: + circuits: The circuits that will run the spectroscopy experiment. + + Raises: + QiskitError: + - If relative frequencies are used but no backend was given. + - If the backend configuration does not define dt. + """ + if not backend and not self._absolute: + raise QiskitError("Cannot run spectroscopy relative to resonator without a backend.") + + # Create a template circuit + freq_param = Parameter("frequency") + duration = self.experiment_options.duration + qubit = self.physical_qubits[0] + + # TODO get this to work with circuits + circuits = [] + for freq in self._frequencies: + with pulse.build(backend=backend, name="spectroscopy") as circuit: + pulse.set_frequency(freq, pulse.measure_channel(qubit)) + pulse.play( + pulse.GaussianSquare(duration, 0.1, 64, duration - 4 * 64), + pulse.measure_channel(qubit) + ) + pulse.acquire(duration, qubit, pulse.MemorySlot(qubit)) + + circuit.metadata = { + "experiment_type": self._type, + "qubit": self.physical_qubits[0], + "xval": freq, + "unit": "Hz", + "amplitude": self.experiment_options.amp, + "duration": self.experiment_options.duration, + "sigma": self.experiment_options.sigma, + "width": self.experiment_options.width, + #"schedule": str(circuit), + "meas_level": self.run_options.meas_level, + "meas_return": self.run_options.meas_return, + } + + try: + circuit.metadata["dt"] = getattr(backend.configuration(), "dt") + except AttributeError as no_dt: + raise QiskitError("Dt parameter is missing in backend configuration") from no_dt + + circuits.append(block_to_schedule(circuit)) + + return circuits diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index b9839cc021..9760b2cb04 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -350,6 +350,37 @@ def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.arra return datum[..., 1] * self.scale, None +class ToAbs(IQPart): + """IQ data post-processing. Take the absolute of the IQ point.""" + + def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.array: + """Take the imaginary part of the IQ data. + + Args: + datum: A 2D or 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + error: An optional 2D or 3D array of shots, qubits, and an error on a complex IQ point + as [real, imaginary]. + + Returns: + A 1D or 2D array, each entry is the absolute value of the given IQ data and error. + """ + if error is not None: + data = np.sqrt(datum[..., 0] ** 2 + datum[..., 1] ** 2) * self.scale + error = np.sqrt(error[..., 0] ** 2 + error[..., 1] ** 2) * self.scale + + if len(data) == 1: + data, error = data[0], error[0] + + return data, error + else: + data = np.sqrt(datum[..., 0] ** 2 + datum[..., 1] ** 2) * self.scale + + if len(data) == 1: + data = data[0] + + return data, None + + class Probability(DataAction): """Count data post processing. This returns the probabilities of the outcome string used to initialize an instance of Probability.""" diff --git a/qiskit_experiments/data_processing/processor_library.py b/qiskit_experiments/data_processing/processor_library.py index e75bd99978..fb58cc2dbc 100644 --- a/qiskit_experiments/data_processing/processor_library.py +++ b/qiskit_experiments/data_processing/processor_library.py @@ -16,17 +16,20 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.data_processor import DataProcessor -from qiskit_experiments.data_processing.nodes import AverageData, Probability, SVD +from qiskit_experiments.data_processing.nodes import AverageData, Probability, SVD, ToAbs def get_to_signal_processor( - meas_level: MeasLevel = MeasLevel.CLASSIFIED, meas_return: str = "avg" + meas_level: MeasLevel = MeasLevel.CLASSIFIED, + meas_return: str = "avg", + dimensionality_reduction = "SVD", ) -> DataProcessor: """Get a DataProcessor that produces a continuous signal given the options. Args: meas_level: The measurement level of the data to process. meas_return: The measurement return (single or avg) of the data to process. + dimensionality_reduction: The method to use to reduce the dimensionality of the data. Returns: An instance of DataProcessor capable of dealing with the given options. @@ -34,13 +37,21 @@ def get_to_signal_processor( Raises: DataProcessorError: if the measurement level is not supported. """ + projectors = { + "SVD": SVD, + "ToAbs": ToAbs, + } + if meas_level == MeasLevel.CLASSIFIED: return DataProcessor("counts", [Probability("1")]) if meas_level == MeasLevel.KERNELED: + + projector = projectors[dimensionality_reduction] + if meas_return == "single": - return DataProcessor("memory", [AverageData(), SVD()]) + return DataProcessor("memory", [AverageData(), projector()]) else: - return DataProcessor("memory", [SVD()]) + return DataProcessor("memory", [projector()]) raise DataProcessorError(f"Unsupported measurement level {meas_level}.")