Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8414ada
add new probability node
Oct 6, 2021
26b5b82
update hamiltonian tomo analysis logic and test
Oct 9, 2021
7a62da0
update error amplification analysis logic and test
Oct 11, 2021
482378c
update drag analysis logic
Oct 11, 2021
fa3537a
update rb data cache
Oct 11, 2021
0f7a504
black&lint
Oct 11, 2021
4c66e3a
add reno
Oct 11, 2021
1ddc994
add test for new probability
Oct 11, 2021
e225ff1
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into upgr…
Oct 11, 2021
3211c4d
fix new test in main branch
Oct 11, 2021
3f64364
update error amplification analysis logic
Oct 11, 2021
1e8750f
fix error in the alpha_0 computation
Oct 11, 2021
cd7238a
set data driven test for fine amp module
Oct 11, 2021
d05a3b0
fix amp in fine amp analysis
Oct 11, 2021
f9a664a
black
Oct 11, 2021
10b00a0
minor update to fine amp guess logic
Oct 11, 2021
ac5b44a
Update qiskit_experiments/curve_analysis/standard_analysis/error_ampl…
nkanazawa1989 Oct 12, 2021
a6570a6
Update qiskit_experiments/curve_analysis/standard_analysis/error_ampl…
nkanazawa1989 Oct 12, 2021
bcb62c5
Update qiskit_experiments/curve_analysis/standard_analysis/error_ampl…
nkanazawa1989 Oct 12, 2021
b74c942
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 12, 2021
b85c159
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 12, 2021
eaa97de
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 12, 2021
a9a43c1
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 12, 2021
5484f99
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 12, 2021
b95d833
documentation update
Oct 12, 2021
e534cea
fix bug and add test for p = 0.5
Oct 12, 2021
c2dfbbe
update rb cache
Oct 12, 2021
3f61e3f
black
Oct 12, 2021
7e0efec
revert test accuracy change
Oct 12, 2021
4290bc4
fix probability test
Oct 12, 2021
aca3d6b
Update qiskit_experiments/data_processing/nodes.py
nkanazawa1989 Oct 13, 2021
f28c924
merge dirichlet probability with probability
Oct 13, 2021
4ee4383
update reno
Oct 13, 2021
62f7d82
update rb cache
Oct 13, 2021
194568b
fix description of outcome arg
Oct 13, 2021
08efb3d
fix data processor unittest
Oct 13, 2021
11f6c58
fix test
Oct 13, 2021
947f952
logic update (assume beta distribution)
Oct 18, 2021
b7c6c06
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into upgr…
Oct 18, 2021
5351a81
update rb cache
Oct 18, 2021
1ceee7e
fix test
Oct 18, 2021
7c8b142
rb cache update
Oct 19, 2021
8c12a8d
Update docstring for Probability node
chriseclectic Oct 25, 2021
93276b7
Merge pull request #4 from chriseclectic/beta-prob
nkanazawa1989 Oct 25, 2021
9f3e946
update rb cache
Oct 25, 2021
da4bf5d
Merge branch 'main' of github.com:Qiskit/qiskit-experiments into upgr…
Oct 25, 2021
9fe8770
fix test
Oct 25, 2021
8291aa8
black
Oct 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import numpy as np

import qiskit_experiments.curve_analysis as curve
from qiskit_experiments.exceptions import CalibrationError


class ErrorAmplificationAnalysis(curve.CurveAnalysis):
Expand All @@ -32,7 +31,29 @@ class ErrorAmplificationAnalysis(curve.CurveAnalysis):

.. math::
y = \frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] \
+{\rm phase\_offset}\right)+{\rm base}
-{\rm phase\_offset}\right)+{\rm base}

To understand how the error is measured we can transformed the function above into

.. math::
y = \frac{{\rm amp}}{2} \left(\
\cos\right({\rm d}\theta \cdot x\left)\
\cos\right({\rm apg} \cdot x - {\rm phase\_offset}\left) -\
\sin\right({\rm d}\theta \cdot x\left)\
\sin\right({\rm apg} \cdot x - {\rm phase\_offset}\left)
\right) + {\rm base}

When :math:`{\rm apg} \cdot x - {\rm phase\_offset} = (2n + 1) \pi/2` is satisfied the
fit model above simplifies to

.. math::
y = \mp \frac{{\rm amp}}{2} \sin\left({\rm d}\theta \cdot x\right) + {\rm base}

In the limit :math:`{\rm d}\theta \ll 1`, the error can be estimated from the curve data

.. math::
{\rm d}\theta \simeq \mp \frac{2(y - {\rm base})}{x \cdot {\rm amp}}


# section: fit_parameters
defpar \rm amp:
Expand All @@ -49,6 +70,8 @@ class ErrorAmplificationAnalysis(curve.CurveAnalysis):
desc: The angle offset in the gate that we wish to measure.
init_guess: Multiple initial guesses are tried ranging from -a to a
where a is given by :code:`max(abs(angle_per_gate), np.pi / 2)`.
Extra guesses are added based on curve data when either :math:`\rm amp` or
:math:`\rm base` is :math:`\pi/2`. See fit model for details.
bounds: [-pi, pi].

# section: note
Expand All @@ -63,7 +86,7 @@ class ErrorAmplificationAnalysis(curve.CurveAnalysis):
x,
amp=0.5 * amp,
freq=(d_theta + angle_per_gate) / (2 * np.pi),
phase=phase_offset,
phase=-phase_offset,
baseline=base,
),
plot_color="blue",
Expand All @@ -86,7 +109,6 @@ def _default_options(cls):
:math:`\pi/2` if the square-root of X gate is added before the repeated gates.
This is decided for the user in :meth:`set_schedule` depending on whether the
sx gate is included in the experiment.
number_of_guesses (int): The number of initial guesses to try.
max_good_angle_error (float): The maximum angle error for which the fit is
considered as good. Defaults to :math:`\pi/2`.
"""
Expand All @@ -96,7 +118,6 @@ def _default_options(cls):
default_options.ylabel = "Population"
default_options.angle_per_gate = None
default_options.phase_offset = 0.0
default_options.number_guesses = 21
default_options.max_good_angle_error = np.pi / 2

return default_options
Expand All @@ -115,8 +136,6 @@ def _generate_fit_guesses(
Raises:
CalibrationError: When ``angle_per_gate`` is missing.
"""
n_guesses = self._get_option("number_guesses")

curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)
max_y, min_y = np.max(curve_data.y), np.min(curve_data.y)
Expand All @@ -128,15 +147,35 @@ def _generate_fit_guesses(
user_opt.p0.set_if_empty(amp=max_y - min_y)
user_opt.bounds.set_if_empty(amp=(-2 * max_abs_y, 2 * max_abs_y))

# Base the initial guess on the intended angle_per_gate.
angle_per_gate = self._get_option("angle_per_gate")

if angle_per_gate is None:
raise CalibrationError("The angle_per_gate was not specified in the analysis options.")
# Base the initial guess on the intended angle_per_gate and phase offset.
apg = self._get_option("angle_per_gate")
phi = self._get_option("phase_offset")

# Prepare logical guess for specific condition (often satisfied)
d_theta_guesses = []

offsets = apg * curve_data.x + phi
amp = user_opt.p0.get("amp", self._get_option("amp"))
for i in range(curve_data.x.size):
xi = curve_data.x[i]
yi = curve_data.y[i]
if np.isclose(offsets[i] % np.pi, np.pi / 2) and xi > 0:
# Condition satisfied: i.e. cos(apg x - phi) = 0
err = -np.sign(np.sin(offsets[i])) * (yi - user_opt.p0["base"]) / (0.5 * amp)
# Validate estimate. This is just the first order term of Maclaurin expansion.
if np.abs(err) < 0.5:
d_theta_guesses.append(err / xi)
else:
# Terminate guess generation because larger d_theta x will start to
# reduce net y value and underestimate the rotation.
break

# Add naive guess for more coverage
guess_range = max(abs(apg), np.pi / 2)
d_theta_guesses.extend(np.linspace(-guess_range, guess_range, 11))

guess_range = max(abs(angle_per_gate), np.pi / 2)
options = []
for d_theta_guess in np.linspace(-guess_range, guess_range, n_guesses):
for d_theta_guess in d_theta_guesses:
new_opt = user_opt.copy()
new_opt.p0.set_if_empty(d_theta=d_theta_guess)
options.append(new_opt)
Expand Down
76 changes: 67 additions & 9 deletions qiskit_experiments/data_processing/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"""Different data analysis steps."""

from abc import abstractmethod
from typing import Any, Dict, List, Optional, Tuple, Union
from numbers import Number
from typing import Any, Dict, List, Optional, Tuple, Union, Sequence
import numpy as np

from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction
Expand Down Expand Up @@ -391,17 +392,72 @@ def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.arra


class Probability(DataAction):
"""Count data post processing. This returns the probabilities of the outcome string
used to initialize an instance of Probability."""
r"""Compute the mean probability of a single measurement outcome from counts.

def __init__(self, outcome: str = "1", validate: bool = True):
This node returns the mean and standard deviation of a single measurement
outcome probability $p$ estimated from the observed counts. The mean and
variance are computed from the posterior Beta distribution
$B(\alpha_0^\prime,\alpha_1^\prime)$ estimated from a Bayesian update
of a prior Beta distribution $B(\alpha_0, \alpha_1)$ given the observed
counts.

The mean and variance of the Beta distribution $B(\alpha_0, \alpha_1)$ are:

.. math::

\text{E}[p] = \frac{\alpha_0}{\alpha_0 + \alpha_1}, \quad
\text{Var}[p] = \frac{\text{E}[p] (1 - \text{E}[p])}{\alpha_0 + \alpha_1 + 1}

Given a prior Beta distribution $B(\alpha_0, \alpha_1)$, the posterior
distribution for the observation of $F$ counts of a given
outcome out of $N$ total shots is a $B(\alpha_0^\prime,\alpha_1^\prime)$ with

.. math::
\alpha_0^\prime = \alpha_0 + F, \quad
\alpha_1^\prime = \alpha_1 + N - F.

.. note::

The default value for the prior distribution is *Jeffery's Prior*
$\alpha_0 = \alpha_1 = 0.5$ which represents ignorance about the true
probability value. Note that for this prior the mean probability estimate
from a finite number of counts can never be exactly 0 or 1. The estimated
mean and variance are given by

.. math::

\text{E}[p] = \frac{F + 0.5}{N + 1}, \quad
\text{Var}[p] = \frac{\text{E}[p] (1 - \text{E}[p])}{N + 2}
"""

def __init__(
self,
outcome: str,
alpha_prior: Union[float, Sequence[float]] = 0.5,
validate: bool = True,
):
"""Initialize a counts to probability data conversion.

Args:
outcome: The bitstring for which to compute the probability which defaults to "1".
outcome: The bitstring for which to return the probability and variance.
alpha_prior: A prior Beta distribution parameter ``[`alpha0, alpha1]``.
If specified as float this will use the same value for
``alpha0`` and``alpha1`` (Default: 0.5).
validate: If set to False the DataAction will not validate its input.

Raises:
DataProcessorError: When the dimension of the prior and expected parameter vector
do not match.
"""
self._outcome = outcome
if isinstance(alpha_prior, Number):
self._alpha_prior = [alpha_prior, alpha_prior]
else:
if validate and len(alpha_prior) != 2:
raise DataProcessorError(
"Prior for probability node must be a float or pair of floats."
)
self._alpha_prior = list(alpha_prior)
super().__init__(validate)

def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict, Any]:
Expand Down Expand Up @@ -472,12 +528,14 @@ def _process(

return np.array(populations), np.array(errors)

def _population_error(self, counts_dict) -> Tuple[float, float]:
def _population_error(self, counts_dict: Dict[str, int]) -> Tuple[float, float]:
"""Helper method"""
shots = sum(counts_dict.values())
p_mean = counts_dict.get(self._outcome, 0.0) / shots
p_var = p_mean * (1 - p_mean) / shots

freq = counts_dict.get(self._outcome, 0)
alpha_posterior = [freq + self._alpha_prior[0], shots - freq + self._alpha_prior[1]]
alpha_sum = sum(alpha_posterior)
p_mean = alpha_posterior[0] / alpha_sum
p_var = p_mean * (1 - p_mean) / (alpha_sum + 1)
return p_mean, np.sqrt(p_var)


Expand Down
10 changes: 5 additions & 5 deletions qiskit_experiments/data_processing/processor_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

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, MinMaxNormalize
from qiskit_experiments.data_processing import nodes


def get_processor(
Expand All @@ -38,16 +38,16 @@ def get_processor(
DataProcessorError: if the measurement level is not supported.
"""
if meas_level == MeasLevel.CLASSIFIED:
return DataProcessor("counts", [Probability("1")])
return DataProcessor("counts", [nodes.Probability("1")])

if meas_level == MeasLevel.KERNELED:
if meas_return == "single":
processor = DataProcessor("memory", [AverageData(axis=1), SVD()])
processor = DataProcessor("memory", [nodes.AverageData(axis=1), nodes.SVD()])
else:
processor = DataProcessor("memory", [SVD()])
processor = DataProcessor("memory", [nodes.SVD()])

if normalize:
processor.append(MinMaxNormalize())
processor.append(nodes.MinMaxNormalize())

return processor

Expand Down
15 changes: 9 additions & 6 deletions qiskit_experiments/library/calibration/analysis/drag_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,19 @@ def _generate_fit_guesses(
beta=(-freq_bound, freq_bound),
base=(-max_abs_y, max_abs_y),
)
user_opt.p0.set_if_empty(amp=-0.5, base=0.5)
user_opt.p0.set_if_empty(base=0.5)

# Drag curves can sometimes be very flat, i.e. averages of y-data
# and min-max do not always make good initial guesses. We therefore add
# 0.5 to the initial guesses.
# 0.5 to the initial guesses. Note that we also set amp=-0.5 because the cosine function
# becomes +1 at zero phase, i.e. optimal beta, in which y data should become zero
# in discriminated measurement level.
options = []
for beta_guess in np.linspace(min_beta, max_beta, 20):
new_opt = user_opt.copy()
new_opt.p0.set_if_empty(beta=beta_guess)
options.append(new_opt)
for amp_guess in (0.5, -0.5):
for beta_guess in np.linspace(min_beta, max_beta, 20):
new_opt = user_opt.copy()
new_opt.p0.set_if_empty(amp=amp_guess, beta=beta_guess)
options.append(new_opt)

return options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ class FineAmplitudeAnalysis(ErrorAmplificationAnalysis):
"""

# The intended angle per gat of the gate being calibrated, e.g. pi for a pi-pulse.
__fixed_parameters__ = ["angle_per_gate", "phase_offset"]

# TODO remove amp from fixed parameter.
__fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"]
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/fine_drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def _default_analysis_options(cls) -> Options:
options = super()._default_analysis_options()
options.normalization = True
options.angle_per_gate = 0.0
options.phase_offset = -np.pi / 2
options.phase_offset = np.pi / 2
options.amp = 1.0

return options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ def _default_options(cls):
"""Return the default analysis options."""
default_options = super()._default_options()
default_options.data_processor = dp.DataProcessor(
input_key="counts", data_actions=[dp.Probability("1"), dp.BasisExpectationValue()]
input_key="counts",
data_actions=[dp.Probability("1"), dp.BasisExpectationValue()],
)
default_options.curve_plotter = "mpl_multiv_canvas"
default_options.xlabel = "Flat top width"
Expand Down Expand Up @@ -266,21 +267,23 @@ def _generate_fit_guesses(

guesses = defaultdict(list)
for control in (0, 1):
# start from Z oscillation
x_data = self._data(series_name=f"x|c={control}")
y_data = self._data(series_name=f"y|c={control}")
z_data = self._data(series_name=f"z|c={control}")

omega_xyz = []
for data in (x_data, y_data, z_data):
ymin, ymax = np.percentile(data.y, [10, 90])
if ymax - ymin < 0.2:
# oscillation amplitude might be almost zero,
# then exclude from average because of lower SNR
continue
Comment on lines +276 to +280
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What prompted this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, I let the FFT module estimate a frequency regardless of the signal amplitude. In some test cases, especially when the signal is really weak, the guess module picks random frequency (because computed probability has been changed), and it hurts frequency guess of averaged oscillation frequency measured in x, y, z basis. So I decided to ignore the guess when SNR is likely low, i.e. it may pick some artifact.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! I wonder if using the actual SNR (min-max)/(sqrt(variance)) might be nice in case someone tries to fit a very small/off resonant rotation with this at some point, so that they can average a lot and still do it? Might be a separate PR

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is nice idea. What is the variance here? Is it the variance of y values np.std(data.y) or one computed from the sampling error data.y_err? If latter one, probably np.mean(data.y_err).

fft_freq = curve.guess.frequency(data.x, data.y)
# oscillation amplitude might be almost zero, then exclude from average
if fft_freq > 0:
omega_xyz.append(fft_freq)
omega_xyz.append(fft_freq)
if omega_xyz:
omega = 2 * np.pi * np.average(omega_xyz)
else:
omega = 0.0
omega = 1e-3

zmin, zmax = np.percentile(z_data.y, [10, 90])
theta = np.arccos(np.sqrt((zmax - zmin) / 2))
Expand Down
8 changes: 8 additions & 0 deletions qiskit_experiments/library/characterization/fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ def _default_experiment_options(cls) -> Options:

return options

@classmethod
def _default_analysis_options(cls) -> Options:
"""Default analysis options."""
options = super()._default_analysis_options()
options.amp = 1.0

return options

def __init__(self, qubit: int, gate: Gate):
"""Setup a fine amplitude experiment on the given qubit.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
from qiskit.quantum_info import Clifford
from qiskit.circuit import Gate

import qiskit_experiments.data_processing as dp
from qiskit_experiments.framework import BaseExperiment, ParallelExperiment, Options
from qiskit_experiments.curve_analysis.data_processing import probability
from .rb_analysis import RBAnalysis
from .clifford_utils import CliffordUtils
from .rb_utils import RBUtils
Expand Down Expand Up @@ -88,7 +88,12 @@ def __init__(

# Set configurable options
self.set_experiment_options(lengths=list(lengths), num_samples=num_samples)
self.set_analysis_options(data_processor=probability(outcome="0" * self.num_qubits))
self.set_analysis_options(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the get function of the data processor library?

Copy link
Copy Markdown
Collaborator Author

@nkanazawa1989 nkanazawa1989 Oct 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked but we cannot use the getter. This is a custom processor for RB, because outcome depends on the number of qubits, and it counts for label "0" instead of "1". Of course we can modify the getter, but this should be done in the separate PR.

data_processor=dp.DataProcessor(
input_key="counts",
data_actions=[dp.Probability(outcome="0" * self.num_qubits)],
)
)

# Set fixed options
self._full_sampling = full_sampling
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
fixes:
- |
Update the :class:`~qiskit_experiments.data_processing.Probability`
data processing node to compute the estimated mean and standard deviation
of a measurement outcome probability using a Bayesian update of a
a Beta distribution prior given the observed measurement outcomes.

The default uninformative prior assumes ignorance about the probability
to be estimated and will prevent the estimated mean from being exactly
0 or 1, and prevent the estimated standard deviation from being 0, which
could cause issues with computing weights during curve fitting. A custom
prior can also be provided if prior information about the probability is
know.
Loading