Skip to content

Commit 8693ba9

Browse files
Rabi (#74)
* * Skeleton of the Rabi experiment. * * Completed the fit function. * * Added test for the rabi * * Added test and fixed variance issue. * * Added draft of end to end test. * * Added TODO. * * data processing and options. * * Updated the options for the Rabi experiment. * * Added more resilience in sigmas processing and a test. * * Improved tests. * * Refactored tests to use mock IQ backends. * Black lint. * * Improved fitting. * * Gave fit function variables comprehensive names. * Docstring, black and lint. * Improved fit guessing. * * black. * * Refactored tests based on IQ data to lighten the code. * Update qiskit_experiments/calibration/experiments/rabi.py Co-authored-by: Naoki Kanazawa <[email protected]> * * Aligned Rabi API to current varsion. * Added some circuit tests. * * Fixed docstring typo. * * Docstring fix. * * Fix error message. * * Refactored mock_backend. * * Added np.integer to Probability node. * * Improved test. * * Fixed docstring and label. * * Aligned Rabi with the curve fitting PR. * * Improved the mock IQ backend. * * Black and lint. * * Added amplitude rounding. * * Added plot labels in defaults. * * Docstrings. * * Changed the label of the report. * * Remove normalization option in Rabi. * * Added normalization option to the Rabi experiment. * * Updated phase initial guesses. * * Moved normalization option from run to experiment options. * * Aligned Rabi with master. * Improved fit guesses. * Improved tests. * * Changed a and b to amp and baseline. * Added an extra test. * * Black. Co-authored-by: Naoki Kanazawa <[email protected]>
1 parent 2cad4e2 commit 8693ba9

File tree

6 files changed

+580
-60
lines changed

6 files changed

+580
-60
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2021.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Experiments used solely for calibrating schedules."""
14+
15+
from .rabi import RabiAnalysis, Rabi
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2021.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Rabi amplitude experiment."""
14+
15+
from typing import Any, Dict, List, Optional, Union
16+
import numpy as np
17+
18+
from qiskit import QiskitError, QuantumCircuit
19+
from qiskit.circuit import Gate, Parameter
20+
from qiskit.qobj.utils import MeasLevel
21+
from qiskit.providers import Backend
22+
import qiskit.pulse as pulse
23+
from qiskit.providers.options import Options
24+
25+
from qiskit_experiments.analysis import (
26+
CurveAnalysis,
27+
CurveAnalysisResult,
28+
SeriesDef,
29+
fit_function,
30+
get_opt_value,
31+
get_opt_error,
32+
)
33+
from qiskit_experiments.base_experiment import BaseExperiment
34+
from qiskit_experiments.data_processing.processor_library import get_to_signal_processor
35+
36+
37+
class RabiAnalysis(CurveAnalysis):
38+
r"""Rabi analysis class based on a fit to a cosine function.
39+
40+
Analyse a Rabi experiment by fitting it to a cosine function
41+
42+
.. math::
43+
y = amp \cos\left(2 \pi {\rm freq} x + {\rm phase}\right) + baseline
44+
45+
Fit Parameters
46+
- :math:`amp`: Amplitude of the oscillation.
47+
- :math:`baseline`: Base line.
48+
- :math:`{\rm freq}`: Frequency of the oscillation. This is the fit parameter of interest.
49+
- :math:`{\rm phase}`: Phase of the oscillation.
50+
51+
Initial Guesses
52+
- :math:`amp`: The maximum y value less the minimum y value.
53+
- :math:`baseline`: The average of the data.
54+
- :math:`{\rm freq}`: The frequency with the highest power spectral density.
55+
- :math:`{\rm phase}`: Zero.
56+
57+
Bounds
58+
- :math:`amp`: [-2, 2] scaled to the maximum signal value.
59+
- :math:`baseline`: [-1, 1] scaled to the maximum signal value.
60+
- :math:`{\rm freq}`: [0, inf].
61+
- :math:`{\rm phase}`: [-pi, pi].
62+
"""
63+
64+
__series__ = [
65+
SeriesDef(
66+
fit_func=lambda x, amp, freq, phase, baseline: fit_function.cos(
67+
x, amp=amp, freq=freq, phase=phase, baseline=baseline
68+
),
69+
plot_color="blue",
70+
)
71+
]
72+
73+
@classmethod
74+
def _default_options(cls):
75+
"""Return the default analysis options.
76+
77+
See :meth:`~qiskit_experiment.analysis.CurveAnalysis._default_options` for
78+
descriptions of analysis options.
79+
"""
80+
default_options = super()._default_options()
81+
default_options.p0 = {"amp": None, "freq": None, "phase": None, "baseline": None}
82+
default_options.bounds = {"amp": None, "freq": None, "phase": None, "baseline": None}
83+
default_options.fit_reports = {"freq": "rate"}
84+
default_options.xlabel = "Amplitude"
85+
default_options.ylabel = "Signal (arb. units)"
86+
87+
return default_options
88+
89+
def _setup_fitting(self, **options) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
90+
"""Fitter options."""
91+
user_p0 = self._get_option("p0")
92+
user_bounds = self._get_option("bounds")
93+
94+
max_abs_y = np.max(np.abs(self._data().y))
95+
96+
# Use a fast Fourier transform to guess the frequency.
97+
fft = np.abs(np.fft.fft(self._data().y - np.average(self._data().y)))
98+
damp = self._data().x[1] - self._data().x[0]
99+
freqs = np.linspace(0.0, 1.0 / (2.0 * damp), len(fft))
100+
101+
b_guess = np.average(self._data().y)
102+
a_guess = np.max(self._data().y) - np.min(self._data().y) - b_guess
103+
f_guess = freqs[np.argmax(fft[0 : len(fft) // 2])]
104+
105+
if user_p0["phase"] is not None:
106+
p_guesses = [user_p0["phase"]]
107+
else:
108+
p_guesses = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4, np.pi]
109+
110+
fit_options = []
111+
for p_guess in p_guesses:
112+
fit_option = {
113+
"p0": {
114+
"amp": user_p0["amp"] or a_guess,
115+
"freq": user_p0["freq"] or f_guess,
116+
"phase": p_guess,
117+
"baseline": user_p0["baseline"] or b_guess,
118+
},
119+
"bounds": {
120+
"amp": user_bounds["amp"] or (-2 * max_abs_y, 2 * max_abs_y),
121+
"freq": user_bounds["freq"] or (0, np.inf),
122+
"phase": user_bounds["phase"] or (-np.pi, np.pi),
123+
"baseline": user_bounds["baseline"] or (-1 * max_abs_y, 1 * max_abs_y),
124+
},
125+
}
126+
fit_option.update(options)
127+
fit_options.append(fit_option)
128+
129+
return fit_options
130+
131+
def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult:
132+
"""Algorithmic criteria for whether the fit is good or bad.
133+
134+
A good fit has:
135+
- a reduced chi-squared lower than three,
136+
- more than a quarter of a full period,
137+
- less than 10 full periods, and
138+
- an error on the fit frequency lower than the fit frequency.
139+
"""
140+
fit_freq = get_opt_value(analysis_result, "freq")
141+
fit_freq_err = get_opt_error(analysis_result, "freq")
142+
143+
criteria = [
144+
analysis_result["reduced_chisq"] < 3,
145+
1.0 / 4.0 < fit_freq < 10.0,
146+
(fit_freq_err is None or (fit_freq_err < fit_freq)),
147+
]
148+
149+
if all(criteria):
150+
analysis_result["quality"] = "computer_good"
151+
else:
152+
analysis_result["quality"] = "computer_bad"
153+
154+
return analysis_result
155+
156+
157+
class Rabi(BaseExperiment):
158+
"""An experiment that scans the amplitude of a pulse to calibrate rotations between 0 and 1.
159+
160+
The circuits that are run have a custom rabi gate with the pulse schedule attached to it
161+
through the calibrations. The circuits are of the form:
162+
163+
.. parsed-literal::
164+
165+
┌───────────┐ ░ ┌─┐
166+
q_0: ┤ Rabi(amp) ├─░─┤M├
167+
└───────────┘ ░ └╥┘
168+
measure: 1/═════════════════╩═
169+
0
170+
171+
"""
172+
173+
__analysis_class__ = RabiAnalysis
174+
175+
@classmethod
176+
def _default_run_options(cls) -> Options:
177+
"""Default option values for the experiment :meth:`run` method."""
178+
return Options(
179+
meas_level=MeasLevel.KERNELED,
180+
meas_return="single",
181+
)
182+
183+
@classmethod
184+
def _default_experiment_options(cls) -> Options:
185+
"""Default values for the pulse if no schedule is given.
186+
187+
Users can set a schedule by doing
188+
189+
.. code-block::
190+
191+
rabi.set_experiment_options(schedule=rabi_schedule)
192+
193+
"""
194+
return Options(
195+
duration=160,
196+
sigma=40,
197+
amplitudes=np.linspace(-0.95, 0.95, 51),
198+
schedule=None,
199+
normalization=True,
200+
)
201+
202+
def __init__(self, qubit: int):
203+
"""Setup a Rabi experiment on the given qubit.
204+
205+
Args:
206+
qubit: The qubit on which to run the Rabi experiment.
207+
"""
208+
super().__init__([qubit])
209+
210+
def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
211+
"""Create the circuits for the Rabi experiment.
212+
213+
Args:
214+
backend: A backend object.
215+
216+
Returns:
217+
A list of circuits with a rabi gate with an attached schedule. Each schedule
218+
will have a different value of the scanned amplitude.
219+
220+
Raises:
221+
QiskitError:
222+
- If the user-provided schedule does not contain a channel with an index
223+
that matches the qubit on which to run the Rabi experiment.
224+
- If the user provided schedule has more than one free parameter.
225+
"""
226+
# TODO this is temporary logic. Need update of circuit data and processor logic.
227+
self.set_analysis_options(
228+
data_processor=get_to_signal_processor(
229+
meas_level=self.run_options.meas_level,
230+
meas_return=self.run_options.meas_return,
231+
normalize=self.experiment_options.normalization,
232+
)
233+
)
234+
235+
schedule = self.experiment_options.get("schedule", None)
236+
237+
if schedule is None:
238+
amp = Parameter("amp")
239+
with pulse.build() as default_schedule:
240+
pulse.play(
241+
pulse.Gaussian(
242+
duration=self.experiment_options.duration,
243+
amp=amp,
244+
sigma=self.experiment_options.sigma,
245+
),
246+
pulse.DriveChannel(self.physical_qubits[0]),
247+
)
248+
249+
schedule = default_schedule
250+
else:
251+
if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels):
252+
raise QiskitError(
253+
f"User provided schedule {schedule.name} does not contain a channel "
254+
"for the qubit on which to run Rabi."
255+
)
256+
257+
if len(schedule.parameters) != 1:
258+
raise QiskitError("Schedule in Rabi must have exactly one free parameter.")
259+
260+
param = next(iter(schedule.parameters))
261+
262+
gate = Gate(name="Rabi", num_qubits=1, params=[param])
263+
264+
circuit = QuantumCircuit(1)
265+
circuit.append(gate, (0,))
266+
circuit.measure_active()
267+
circuit.add_calibration(gate, (self.physical_qubits[0],), schedule, params=[param])
268+
269+
circs = []
270+
for amp in self.experiment_options.amplitudes:
271+
amp = np.round(amp, decimals=6)
272+
assigned_circ = circuit.assign_parameters({param: amp}, inplace=False)
273+
assigned_circ.metadata = {
274+
"experiment_type": self._type,
275+
"qubit": self.physical_qubits[0],
276+
"xval": amp,
277+
"unit": "arb. unit",
278+
"amplitude": amp,
279+
"schedule": str(schedule),
280+
}
281+
282+
if backend:
283+
assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt", "n.a.")
284+
285+
circs.append(assigned_circ)
286+
287+
return circs

qiskit_experiments/data_processing/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict,
440440
f"Key {bit_str} is not a valid count key in{self.__class__.__name__}."
441441
)
442442

443-
if not isinstance(count, (int, float)):
443+
if not isinstance(count, (int, float, np.integer)):
444444
raise DataProcessorError(
445445
f"Count {bit_str} is not a valid count value in {self.__class__.__name__}."
446446
)

0 commit comments

Comments
 (0)