-
Notifications
You must be signed in to change notification settings - Fork 130
Spectroscopy #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Spectroscopy #31
Changes from 10 commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
1a68a65
* First draft of spectroscopy.
eggerdj c6497e9
* Changed counts to memory for Level1 data.
eggerdj fa1ee89
* Switched to _physical_qubits[0]
eggerdj 2e54b61
* Added user provided initial guesses.
eggerdj 21b9c74
* Black.
eggerdj 8eaccf0
* Added fit boundaries.
eggerdj 7f7e8aa
* Black and Lint.
eggerdj fbb81f5
* Improved docstring.
eggerdj 3c33538
* Fixed default parameters.
eggerdj b60de51
* Fixed array bug.
eggerdj 1f19f3d
* Added optional to type hints.
eggerdj c9791c8
* Black.
eggerdj a8ba063
* Added tests
eggerdj 3301a17
* Updated guesses.
eggerdj b167e23
* Changed default to / 4 instead of / 5
eggerdj fa334da
* Added circuit information to the metadata.
eggerdj 846af31
* Made frequency a parameter in the gate.
eggerdj 2937795
* Added dt to metadata and improved docstring.
eggerdj 44f7400
* Improved algorithmic criteria for the fit quality.
eggerdj 4ddb9b9
Merge branch 'main' into spectroscopy
eggerdj 9dd5bf8
* Moved spectroscopy to characterization.
eggerdj 3b0f35d
* Added better defaults for fit quality.
eggerdj 47f4e50
* Black.
eggerdj 7f9a5b1
* Updated to recent ExperimentData change.
eggerdj 1bfe7d6
* Test.
eggerdj de6eac3
* Added trailling kwargs
eggerdj aaa0c40
* Lint.
eggerdj cb1f52b
* Temporary commit to debug CI.
eggerdj 887f012
* Removing previous commit.
eggerdj 7a80b2a
* Added TestJob to the test.
eggerdj 43d1603
Merge branch 'main' into spectroscopy
eggerdj 6466f0a
* Removed self._absolute in the metadata.
eggerdj e8fbbf5
Merge branch 'spectroscopy' of github.com:eggerdj/qiskit-experiments …
eggerdj 6173531
* physical_qubits.
eggerdj 66b49ca
* Changed bounds from list to tuple.
eggerdj fa13a8a
* Temporarily removed Excpet to figure out why CI is failing.
eggerdj 3cf6c6e
* Added HAS_MATPLOTLIB
eggerdj fd934a1
* Added back the exception.
eggerdj d1b1c9b
* Fixed sqrt issue on sigma.
eggerdj 7138dae
Merge branch 'main' into spectroscopy
eggerdj 6c7275f
* Small refactoring of fitting.
eggerdj fcc0489
Merge branch 'main' into spectroscopy
eggerdj 4085945
* Built in SVD.
eggerdj ec81cb5
* Working on tests.
eggerdj 5307ae6
* Improved tests.
eggerdj 58a53db
* Doctrings.
eggerdj 3cf9d65
* Lint and plotting.
eggerdj 42886fa
* Fixed docstring in tests.
eggerdj 69087dd
* Made sure data processor error returns the standard deviation.
eggerdj d4b8670
* Singled out reusable part of spectroscopy test.
eggerdj 10aeb05
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into spec…
eggerdj c7de4c1
* Added pulse parameters as kwargs.
eggerdj cb8e5e2
* Drive channel.
eggerdj 3f43c74
* Renamed Spectroscopy to QubitSpectroscopy.
eggerdj 717dce0
* Outsourced the getting of a data processor to a class method in Dat…
eggerdj d5b1128
* Fixed the docs.
eggerdj 37a6d5c
* Obtaining dt from the backend.
eggerdj 0ca6ce1
Merge branch 'main' into spectroscopy
eggerdj b7521b6
Merge branch 'main' into spectroscopy
eggerdj 75e4063
* Moved pulse options into experiment options and out of run options.
eggerdj 4409442
Merge branch 'spectroscopy' of github.com:eggerdj/qiskit-experiments …
eggerdj 9b14031
* Started a data processor library.
eggerdj 42b5cc6
* Added test for the average on spectroscopy.
eggerdj c83a2fc
Merge branch 'main' into spectroscopy
eggerdj 026b71d
Update qiskit_experiments/characterization/qubit_spectroscopy.py
eggerdj dd23d10
* removed np.random.seed(0).
eggerdj 1b5641b
Merge branch 'main' into spectroscopy
eggerdj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ | |
"pass_manager", | ||
"callback", | ||
"output_name", | ||
"meas_level" | ||
} | ||
|
||
|
||
|
284 changes: 284 additions & 0 deletions
284
qiskit_experiments/calibration/experiments/spectroscopy.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
# 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.""" | ||
|
||
from typing import List, Optional, Tuple, Union | ||
import numpy as np | ||
|
||
from qiskit import QuantumCircuit | ||
from qiskit.circuit import Gate | ||
import qiskit.pulse as pulse | ||
from qiskit.qobj.utils import MeasLevel | ||
from qiskit_experiments.analysis.curve_fitting import curve_fit | ||
from qiskit_experiments.base_analysis import BaseAnalysis | ||
from qiskit_experiments.base_experiment import BaseExperiment | ||
from qiskit_experiments import AnalysisResult | ||
from qiskit_experiments.data_processing.data_processor import DataProcessor | ||
from qiskit_experiments.data_processing.nodes import ToReal | ||
from qiskit_experiments.data_processing.nodes import Probability | ||
|
||
|
||
class SpectroscopyAnalysis(BaseAnalysis): | ||
"""Class to analysis a spectroscopy experiment.""" | ||
|
||
# pylint: disable=arguments-differ | ||
def _run_analysis( | ||
self, | ||
experiment_data, | ||
data_processor=None, | ||
meas_level=MeasLevel.KERNELED, | ||
amp_guess: float = None, | ||
gamma_guesses: List[float] = None, | ||
freq_guess: float = None, | ||
offset_guess: float = None, | ||
amplitude_bounds: List[float] = None, | ||
width_bounds: List[float] = None, | ||
freq_bounds: List[float] = None, | ||
offset_bounds: List[float] = None, | ||
) -> Tuple[AnalysisResult, None]: | ||
""" | ||
Analyse a spectroscopy experiment by fitting the data to a Lorentz function. | ||
The fit function is: | ||
|
||
.. math:: | ||
|
||
a * ( g**2 / ((x-x0)**2 + g**2)) + b | ||
|
||
Here, :math:`x` is the frequency. The analysis loops over the initial guesses | ||
of the width parameter :math:`g`. | ||
|
||
Args: | ||
experiment_data: The experiment data to analyze. | ||
data_processor: The data processor with which to process the data. | ||
meas_level: The measurement level of the experiment data. | ||
amp_guess: The amplitude of the Lorentz function, i.e. :math:`a`. If not | ||
provided, this will default to the maximum absolute value of the ydata. | ||
gamma_guesses: The guesses for the width parameter of the Lorentz distribution, | ||
i.e. :math:`g`. If it is not given this will default to an array of ten | ||
points linearly spaced between zero and the full width of the data. | ||
freq_guess: A guess for the frequency of the peak :math:`x0`. If not provided | ||
this guess will default to the location of the highest absolute data point. | ||
offset_guess: A guess for the magnitude :math:`b` offset of the fit function. | ||
If not provided, the initial guess defaults to the average of the ydata. | ||
amplitude_bounds: Bounds on the amplitude of the Lorentz function as a list of | ||
two floats. The default bounds are [0, 1.1*max(ydata)] | ||
width_bounds: Bounds on the width of the Lorentz function as a list of two floats. | ||
The default values are [0, frequency range]. | ||
freq_bounds: Bounds on the center frequency as a list of two floats. The default | ||
values are 90% of the lower end of the frequency and 110% of the upper end of | ||
the frequency. | ||
offset_bounds: Bounds on the offset of the Lorentz function as a list of two floats. | ||
The default values are the minimum and maximum of the ydata. | ||
|
||
Returns: | ||
The analysis result with the estimated peak frequency. | ||
|
||
Raises: | ||
ValueError: If the measurement level is not supported. | ||
""" | ||
|
||
# Pick a data processor. | ||
if data_processor is None: | ||
if meas_level == MeasLevel.CLASSIFIED: | ||
data_processor = DataProcessor("counts", [Probability("1")]) | ||
elif meas_level == MeasLevel.KERNELED: | ||
data_processor = DataProcessor("memory", [ToReal()]) | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
raise ValueError("Unsupported measurement level.") | ||
|
||
y_sigmas = np.array([data_processor(datum) for datum in experiment_data.data]) | ||
sigmas = y_sigmas[:, 1] | ||
ydata = abs(y_sigmas[:, 0]) | ||
xdata = np.array([datum["metadata"]["xval"] for datum in experiment_data.data]) | ||
|
||
if not offset_guess: | ||
offset_guess = np.average(ydata) | ||
if not amp_guess: | ||
amp_guess = np.max(ydata) | ||
if not freq_guess: | ||
peak_idx = np.argmax(ydata) | ||
freq_guess = xdata[peak_idx] | ||
if not gamma_guesses: | ||
gamma_guesses = np.linspace(0, abs(xdata[-1] - xdata[0]), 10) | ||
if amplitude_bounds is None: | ||
amplitude_bounds = [0.0, 1.1 * max(ydata)] | ||
if width_bounds is None: | ||
width_bounds = [0, abs(xdata[-1] - xdata[0])] | ||
if freq_bounds is None: | ||
freq_bounds = [0.9 * xdata[0], 1.1 * xdata[-1]] | ||
if offset_bounds is None: | ||
offset_bounds = [np.min(ydata), np.max(ydata)] | ||
|
||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
best_fit = None | ||
|
||
lower = np.array([amplitude_bounds[0], width_bounds[0], freq_bounds[0], offset_bounds[0]]) | ||
upper = np.array([amplitude_bounds[1], width_bounds[1], freq_bounds[1], offset_bounds[1]]) | ||
|
||
for gamma_guess in gamma_guesses: | ||
fit_result = curve_fit( | ||
lambda x, a, g, x0, b: a * (g ** 2 / ((x - x0) ** 2 + g ** 2)) + b, | ||
xdata, | ||
np.array(ydata), | ||
np.array([amp_guess, gamma_guess, freq_guess, offset_guess]), | ||
np.array(sigmas), | ||
(lower, upper), | ||
) | ||
|
||
if not best_fit: | ||
best_fit = fit_result | ||
else: | ||
if fit_result["reduced_chisq"] < best_fit["reduced_chisq"]: | ||
best_fit = fit_result | ||
|
||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
analysis_result = AnalysisResult( | ||
{ | ||
"value": best_fit["popt"][2], | ||
"stderr": best_fit["popt_err"][2], | ||
"unit": experiment_data.data[0]["metadata"].get("unit", "Hz"), | ||
"label": "Spectroscopy", | ||
"fit": best_fit, | ||
"quality": self._fit_quality( | ||
best_fit["popt"], | ||
best_fit["popt_err"], | ||
best_fit["reduced_chisq"], | ||
xdata[0], | ||
xdata[-1], | ||
), | ||
} | ||
) | ||
|
||
return analysis_result, None | ||
|
||
@staticmethod | ||
def _fit_quality(fit_out, fit_err, reduced_chisq, min_freq, max_freq) -> str: | ||
""" | ||
Algorithmic criteria for whether the fit is good or bad. | ||
A good fit has a small reduced chi-squared and the peak must be | ||
within the scanned frequency range. | ||
|
||
Args: | ||
fit_out: Value of the fit. | ||
fit_err: Errors on the fit value. | ||
reduced_chisq: Reduced chi-squared of the fit. | ||
min_freq: Minimum frequency in the spectroscopy. | ||
max_freq: Maximum frequency in the spectroscopy. | ||
|
||
Returns: | ||
computer_bad or computer_good if the fit passes or fails, respectively. | ||
""" | ||
|
||
if ( | ||
min_freq <= fit_out[2] <= max_freq | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
and fit_out[1] < (max_freq - min_freq) | ||
and reduced_chisq < 3 | ||
and (fit_err[2] is None or fit_err[2] < fit_out[2]) | ||
): | ||
return "computer_good" | ||
else: | ||
return "computer_bad" | ||
|
||
|
||
class Spectroscopy(BaseExperiment): | ||
"""Class the runs spectroscopy by sweeping the qubit frequency.""" | ||
|
||
__analysis_class__ = SpectroscopyAnalysis | ||
|
||
# Supported units for spectroscopy. | ||
__units__ = {"Hz": 1.0, "kHz": 1.0e3, "MHz": 1.0e6, "GHz": 1.0e9} | ||
|
||
# default run options | ||
__run_defaults__ = {"meas_level": MeasLevel.KERNELED} | ||
|
||
def __init__( | ||
self, qubit: int, frequency_shifts: Union[List[float], np.array], unit: Optional[str] = "Hz" | ||
): | ||
""" | ||
A spectroscopy experiment run by shifting the frequency of the qubit. | ||
The parameters of the GaussianSquare spectroscopy pulse are 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 5 x duration. | ||
- 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. | ||
frequency_shifts: The frequencies to scan in the experiment. | ||
unit: The unit in which the user specifies the frequencies. Can be one | ||
of 'Hz', 'kHz', 'MHz', 'GHz'. Internally, all frequencies will be converted | ||
to 'Hz'. | ||
|
||
Raises: | ||
ValueError: if there are less than three frequency shifts or if the unit is not known. | ||
|
||
""" | ||
if len(frequency_shifts) < 3: | ||
raise ValueError("Spectroscopy requires at least three frequencies.") | ||
|
||
if unit not in self.__units__: | ||
raise ValueError(f"Unsupported unit: {unit}.") | ||
|
||
self._frequency_shifts = [freq * self.__units__[unit] for freq in frequency_shifts] | ||
|
||
super().__init__([qubit], circuit_options=("amp", "duration", "sigma", "width")) | ||
|
||
# pylint: disable=unused-argument | ||
def circuits(self, backend: Optional["Backend"] = None, **circuit_options): | ||
""" | ||
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. | ||
circuit_options: Key word arguments to run the circuits. The circuit options are | ||
- amp: The amplitude of the GaussianSquare pulse, defaults to 0.1. | ||
- duration: The duration of the GaussianSquare pulse, defaults to 10240. | ||
- sigma: The standard deviation of the GaussianSquare pulse, defaults to one | ||
fith of the duration. | ||
- width: The width of the flat top in the GaussianSquare pulse, defaults to 0. | ||
|
||
Returns: | ||
circuits: The circuits that will run the spectroscopy experiment. | ||
""" | ||
|
||
amp = circuit_options.get("amp", 0.1) | ||
duration = circuit_options.get("duration", 1024) | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sigma = circuit_options.get("sigma", duration / 5) | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
width = circuit_options.get("width", 0) | ||
|
||
drive = pulse.DriveChannel(self._physical_qubits[0]) | ||
|
||
circs = [] | ||
|
||
for freq_shift in self._frequency_shifts: | ||
with pulse.build(name=f"Frequency shift{freq_shift}") as sched: | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pulse.shift_frequency(freq_shift, drive) | ||
pulse.play(pulse.GaussianSquare(duration, amp, sigma, width), drive) | ||
|
||
gate = Gate(name="Spec", num_qubits=1, params=[]) | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
circuit = QuantumCircuit(1) | ||
circuit.append(gate, (0,)) | ||
circuit.add_calibration(gate, (self._physical_qubits[0],), sched) | ||
circuit.measure_active() | ||
|
||
circuit.metadata = { | ||
eggerdj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"experiment_type": self._type, | ||
"qubit": self._physical_qubits[0], | ||
"xval": freq_shift, | ||
"unit": "Hz", | ||
} | ||
|
||
circs.append(circuit) | ||
|
||
return circs |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.