Skip to content

Rabi #74

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 50 commits into from
Jun 17, 2021
Merged

Rabi #74

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
dfcec7b
* Skeleton of the Rabi experiment.
eggerdj May 18, 2021
ef7626d
* Completed the fit function.
eggerdj May 19, 2021
da25db1
* Added test for the rabi
eggerdj May 19, 2021
3af3d7e
* Added test and fixed variance issue.
eggerdj May 19, 2021
1910ca3
* Added draft of end to end test.
eggerdj May 19, 2021
0eb7027
* Added TODO.
eggerdj May 30, 2021
387f270
* data processing and options.
eggerdj Jun 1, 2021
134f98f
Merge branch 'main' into rabi
eggerdj Jun 1, 2021
287e93f
* Updated the options for the Rabi experiment.
eggerdj Jun 1, 2021
524e546
* Added more resilience in sigmas processing and a test.
eggerdj Jun 2, 2021
30e5c73
* Improved tests.
eggerdj Jun 2, 2021
1ccfbda
Merge branch 'main' into rabi
eggerdj Jun 2, 2021
4efd868
* Refactored tests to use mock IQ backends.
eggerdj Jun 2, 2021
fc53c19
* Improved fitting.
eggerdj Jun 3, 2021
93598e1
* Gave fit function variables comprehensive names.
eggerdj Jun 3, 2021
4dc661d
* black.
eggerdj Jun 3, 2021
4a6273e
* Refactored tests based on IQ data to lighten the code.
eggerdj Jun 3, 2021
e9de9c4
Update qiskit_experiments/calibration/experiments/rabi.py
eggerdj Jun 7, 2021
5d23300
* Aligned Rabi API to current varsion.
eggerdj Jun 7, 2021
08a6bc4
* Fixed docstring typo.
eggerdj Jun 7, 2021
d34b620
* Docstring fix.
eggerdj Jun 7, 2021
cd56b0a
* Fix error message.
eggerdj Jun 7, 2021
4a6040b
* Refactored mock_backend.
eggerdj Jun 7, 2021
de8695e
* Added np.integer to Probability node.
eggerdj Jun 7, 2021
217a27b
* Improved test.
eggerdj Jun 7, 2021
d8d0696
* Fixed docstring and label.
eggerdj Jun 7, 2021
b8f9d48
Merge branch 'main' into rabi
eggerdj Jun 7, 2021
28a8f6c
Merge branch 'main' into rabi
eggerdj Jun 8, 2021
e28d023
* Aligned Rabi with the curve fitting PR.
eggerdj Jun 9, 2021
0260f96
* Improved the mock IQ backend.
eggerdj Jun 10, 2021
ee0d555
Merge branch 'main' into rabi
eggerdj Jun 14, 2021
c9dd7e0
* Black and lint.
eggerdj Jun 14, 2021
869ba83
Merge branch 'rabi' of github.com:eggerdj/qiskit-experiments into rabi
eggerdj Jun 14, 2021
cdc9013
* Added amplitude rounding.
eggerdj Jun 14, 2021
6e66824
Merge branch 'main' into rabi
eggerdj Jun 14, 2021
2653dde
Merge branch 'rabi' of github.com:eggerdj/qiskit-experiments into rabi
eggerdj Jun 14, 2021
5d06c5e
* Added plot labels in defaults.
eggerdj Jun 14, 2021
480899c
* Docstrings.
eggerdj Jun 14, 2021
6b48e5c
* Changed the label of the report.
eggerdj Jun 15, 2021
6197bfc
* Remove normalization option in Rabi.
eggerdj Jun 15, 2021
5bf30ec
* Added normalization option to the Rabi experiment.
eggerdj Jun 15, 2021
84e3c36
* Updated phase initial guesses.
eggerdj Jun 15, 2021
505070b
* Moved normalization option from run to experiment options.
eggerdj Jun 15, 2021
106c533
Merge branch 'main' into rabi
eggerdj Jun 15, 2021
67ba525
Merge branch 'main' into rabi
eggerdj Jun 16, 2021
f05e347
* Aligned Rabi with master.
eggerdj Jun 16, 2021
83266a5
Merge branch 'main' into rabi
eggerdj Jun 17, 2021
3f1d2bc
* Changed a and b to amp and baseline.
eggerdj Jun 17, 2021
ceadc0c
Merge branch 'rabi' of github.com:eggerdj/qiskit-experiments into rabi
eggerdj Jun 17, 2021
ad5cfb5
* Black.
eggerdj Jun 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions qiskit_experiments/calibration/experiments/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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.

"""Experiments used solely for calibrating schedules."""

from .rabi import RabiAnalysis, Rabi
287 changes: 287 additions & 0 deletions qiskit_experiments/calibration/experiments/rabi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# 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.

"""Rabi amplitude experiment."""

from typing import Any, Dict, List, Optional, Union
import numpy as np

from qiskit import QiskitError, QuantumCircuit
from qiskit.circuit import Gate, Parameter
from qiskit.qobj.utils import MeasLevel
from qiskit.providers import Backend
import qiskit.pulse as pulse
from qiskit.providers.options import Options

from qiskit_experiments.analysis import (
CurveAnalysis,
CurveAnalysisResult,
SeriesDef,
fit_function,
get_opt_value,
get_opt_error,
)
from qiskit_experiments.base_experiment import BaseExperiment
from qiskit_experiments.data_processing.processor_library import get_to_signal_processor


class RabiAnalysis(CurveAnalysis):
r"""Rabi analysis class based on a fit to a cosine function.

Analyse a Rabi experiment by fitting it to a cosine function

.. math::
y = amp \cos\left(2 \pi {\rm freq} x + {\rm phase}\right) + baseline

Fit Parameters
- :math:`amp`: Amplitude of the oscillation.
- :math:`baseline`: Base line.
- :math:`{\rm freq}`: Frequency of the oscillation. This is the fit parameter of interest.
- :math:`{\rm phase}`: Phase of the oscillation.

Initial Guesses
- :math:`amp`: The maximum y value less the minimum y value.
- :math:`baseline`: The average of the data.
- :math:`{\rm freq}`: The frequency with the highest power spectral density.
- :math:`{\rm phase}`: Zero.

Bounds
- :math:`amp`: [-2, 2] scaled to the maximum signal value.
- :math:`baseline`: [-1, 1] scaled to the maximum signal value.
- :math:`{\rm freq}`: [0, inf].
- :math:`{\rm phase}`: [-pi, pi].
"""

__series__ = [
SeriesDef(
fit_func=lambda x, amp, freq, phase, baseline: fit_function.cos(
x, amp=amp, freq=freq, phase=phase, baseline=baseline
),
plot_color="blue",
)
]

@classmethod
def _default_options(cls):
"""Return the default analysis options.

See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for
descriptions of analysis options.
"""
default_options = super()._default_options()
default_options.p0 = {"amp": None, "freq": None, "phase": None, "baseline": None}
default_options.bounds = {"amp": None, "freq": None, "phase": None, "baseline": None}
default_options.fit_reports = {"freq": "rate"}
default_options.xlabel = "Amplitude"
default_options.ylabel = "Signal (arb. units)"

return default_options

def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
"""Fitter options."""
user_p0 = self._get_option("p0")
user_bounds = self._get_option("bounds")

max_abs_y = np.max(np.abs(self._data().y))

# Use a fast Fourier transform to guess the frequency.
fft = np.abs(np.fft.fft(self._data().y - np.average(self._data().y)))
damp = self._data().x[1] - self._data().x[0]
freqs = np.linspace(0.0, 1.0 / (2.0 * damp), len(fft))

b_guess = np.average(self._data().y)
a_guess = np.max(self._data().y) - np.min(self._data().y) - b_guess
f_guess = freqs[np.argmax(fft[0 : len(fft) // 2])]

if user_p0["phase"] is not None:
p_guesses = [user_p0["phase"]]
else:
p_guesses = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4, np.pi]

fit_options = []
for p_guess in p_guesses:
fit_option = {
"p0": {
"amp": user_p0["amp"] or a_guess,
"freq": user_p0["freq"] or f_guess,
"phase": p_guess,
"baseline": user_p0["baseline"] or b_guess,
},
"bounds": {
"amp": user_bounds["amp"] or (-2 * max_abs_y, 2 * max_abs_y),
"freq": user_bounds["freq"] or (0, np.inf),
"phase": user_bounds["phase"] or (-np.pi, np.pi),
"baseline": user_bounds["baseline"] or (-1 * max_abs_y, 1 * max_abs_y),
},
}
fit_option.update(options)
fit_options.append(fit_option)

return fit_options

def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult:
"""Algorithmic criteria for whether the fit is good or bad.

A good fit has:
- a reduced chi-squared lower than three,
- more than a quarter of a full period,
- less than 10 full periods, and
- an error on the fit frequency lower than the fit frequency.
"""
fit_freq = get_opt_value(analysis_result, "freq")
fit_freq_err = get_opt_error(analysis_result, "freq")

criteria = [
analysis_result["reduced_chisq"] < 3,
1.0 / 4.0 < fit_freq < 10.0,
(fit_freq_err is None or (fit_freq_err < fit_freq)),
]

if all(criteria):
analysis_result["quality"] = "computer_good"
else:
analysis_result["quality"] = "computer_bad"

return analysis_result


class Rabi(BaseExperiment):
"""An experiment that scans the amplitude of a pulse to calibrate rotations between 0 and 1.

The circuits that are run have a custom rabi gate with the pulse schedule attached to it
through the calibrations. The circuits are of the form:

.. parsed-literal::

┌───────────┐ ░ ┌─┐
q_0: ┤ Rabi(amp) ├─░─┤M├
└───────────┘ ░ └╥┘
measure: 1/═════════════════╩═
0

"""

__analysis_class__ = RabiAnalysis

@classmethod
def _default_run_options(cls) -> Options:
"""Default option values for the experiment :meth:`run` method."""
return Options(
meas_level=MeasLevel.KERNELED,
meas_return="single",
)

@classmethod
def _default_experiment_options(cls) -> Options:
"""Default values for the pulse if no schedule is given.

Users can set a schedule by doing

.. code-block::

rabi.set_experiment_options(schedule=rabi_schedule)

"""
return Options(
duration=160,
sigma=40,
amplitudes=np.linspace(-0.95, 0.95, 51),
schedule=None,
normalization=True,
)

def __init__(self, qubit: int):
"""Setup a Rabi experiment on the given qubit.

Args:
qubit: The qubit on which to run the Rabi experiment.
"""
super().__init__([qubit])

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the Rabi experiment.

Args:
backend: A backend object.

Returns:
A list of circuits with a rabi gate with an attached schedule. Each schedule
will have a different value of the scanned amplitude.

Raises:
QiskitError:
- If the user-provided schedule does not contain a channel with an index
that matches the qubit on which to run the Rabi experiment.
- If the user provided schedule has more than one free parameter.
"""
# TODO this is temporary logic. Need update of circuit data and processor logic.
self.set_analysis_options(
data_processor=get_to_signal_processor(
meas_level=self.run_options.meas_level,
meas_return=self.run_options.meas_return,
normalize=self.experiment_options.normalization,
)
)

schedule = self.experiment_options.get("schedule", None)

if schedule is None:
amp = Parameter("amp")
with pulse.build() as default_schedule:
pulse.play(
pulse.Gaussian(
duration=self.experiment_options.duration,
amp=amp,
sigma=self.experiment_options.sigma,
),
pulse.DriveChannel(self.physical_qubits[0]),
)

schedule = default_schedule
else:
if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels):
raise QiskitError(
f"User provided schedule {schedule.name} does not contain a channel "
"for the qubit on which to run Rabi."
)

if len(schedule.parameters) != 1:
raise QiskitError("Schedule in Rabi must have exactly one free parameter.")

param = next(iter(schedule.parameters))

gate = Gate(name="Rabi", num_qubits=1, params=[param])

circuit = QuantumCircuit(1)
circuit.append(gate, (0,))
circuit.measure_active()
circuit.add_calibration(gate, (self.physical_qubits[0],), schedule, params=[param])

circs = []
for amp in self.experiment_options.amplitudes:
amp = np.round(amp, decimals=6)
assigned_circ = circuit.assign_parameters({param: amp}, inplace=False)
assigned_circ.metadata = {
"experiment_type": self._type,
"qubit": self.physical_qubits[0],
"xval": amp,
"unit": "arb. unit",
"amplitude": amp,
"schedule": str(schedule),
}

if backend:
assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt", "n.a.")

circs.append(assigned_circ)

return circs
2 changes: 1 addition & 1 deletion qiskit_experiments/data_processing/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict,
f"Key {bit_str} is not a valid count key in{self.__class__.__name__}."
)

if not isinstance(count, (int, float)):
if not isinstance(count, (int, float, np.integer)):
raise DataProcessorError(
f"Count {bit_str} is not a valid count value in {self.__class__.__name__}."
)
Expand Down
Loading