Skip to content
This repository was archived by the owner on Dec 7, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a281f69
add sine test and circuit test
Cryoris Dec 18, 2019
cc1734b
add docstring
Cryoris Dec 18, 2019
222b851
add docstring
Cryoris Dec 18, 2019
cb460ec
add setter for i_objective, tests for setting of A/Q/i_obj
Cryoris Dec 18, 2019
0f32b75
add test for updating A operator, test for IQAE circuit
Cryoris Dec 18, 2019
af9e1a7
fix safe_max/min
Cryoris Dec 19, 2019
d4b3182
only apply Q if the power is not 0
Cryoris Dec 20, 2019
04eaa5a
only apply Q if the power is not 0
Cryoris Dec 20, 2019
62c6d70
evaluation schedule must include Q^0
Cryoris Dec 20, 2019
ba71eb3
add test for MLAE circuits
Cryoris Dec 20, 2019
08e7974
CI is based on 'mle' not 'estimation'
Cryoris Dec 20, 2019
e36a20c
correct definition of bubbles / add comments
Cryoris Dec 20, 2019
8ad95d0
remove unnecessary definition, y should be int
Cryoris Dec 20, 2019
8256230
consistent order: A op, Q op, i_obj
Cryoris Dec 20, 2019
49da38f
fix typo
Cryoris Dec 20, 2019
7578226
fix bug: MLE not in CI
Cryoris Dec 20, 2019
8f4ecf0
update default num. of evaluations
Cryoris Dec 20, 2019
ec0accf
update comments
Cryoris Dec 22, 2019
d79f606
add docstrings, rename ci -> confint throughout
Cryoris Dec 22, 2019
8530532
add "ae" to good-names, well-known, precise variable name for Amplitu…
Cryoris Dec 22, 2019
6a987f4
remove too complex cases, add docstrings
Cryoris Dec 22, 2019
eef5a2a
fix expected value after MLAE fix
Cryoris Dec 22, 2019
a3277e6
add confidence interval tests
Cryoris Dec 22, 2019
8ababcb
fix spell/lint/style
Cryoris Dec 22, 2019
bdfb98d
fix spell/lint/style
Cryoris Dec 22, 2019
067f18f
fix test
Cryoris Dec 22, 2019
3e1dc75
resolve merge conflict
Cryoris Dec 23, 2019
d143dd5
resolve merge conflicts from type hints
Cryoris Jan 6, 2020
36e17ac
rename log_max_evals to m
Cryoris Jan 24, 2020
03900bc
add num_oracle_queries for all AE variants
Cryoris Jan 24, 2020
4b84299
Merge branch 'master' of https://github.com/Qiskit/qiskit-aqua into a…
Cryoris Jan 24, 2020
66b77ea
remove QAES -- not part of this PR
Cryoris Jan 24, 2020
b53f231
fix lints, docstrings begin first line
Cryoris Jan 24, 2020
871d908
fix lint
manoelmarques Jan 24, 2020
fc5ec8f
fix docstring
manoelmarques Jan 24, 2020
3ff5958
fix docstring
manoelmarques Jan 24, 2020
497fc9d
add type hints, fix args order in ae.py
Cryoris Jan 24, 2020
86f977b
avoid using `ae` (use `qae` instead)
Cryoris Jan 26, 2020
34420c7
rename `m` -> `num_oracle_circuit`
Cryoris Jan 26, 2020
9688c65
fix spell & style
Cryoris Jan 26, 2020
2579ba2
test circuit unitaries instead of gate counts
Cryoris Jan 26, 2020
b096b1a
Merge branch 'ae_tests' of github.com:Cryoris/qiskit-aqua into ae_tests
Cryoris Jan 26, 2020
5618cb1
fix lint
Cryoris Jan 26, 2020
9737cd2
Merge branch 'master' into ae_tests
manoelmarques Jan 27, 2020
ddd1db6
Merge branch 'master' into ae_tests
manoelmarques Jan 28, 2020
3fd0891
update docstring
Cryoris Jan 29, 2020
74f9c25
Merge branch 'ae_tests' of github.com:Cryoris/qiskit-aqua into ae_tests
Cryoris Jan 29, 2020
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
177 changes: 124 additions & 53 deletions qiskit/aqua/algorithms/single_sample/amplitude_estimation/ae.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@
# 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.
"""
The Amplitude Estimation Algorithm.
"""
"""The Quantum Phase Estimation-based Amplitude Estimation algorithm."""

from typing import Optional
from typing import Optional, Union, List, Tuple
import logging
from collections import OrderedDict
import numpy as np
from scipy.stats import chi2, norm
from scipy.optimize import bisect

from qiskit import QuantumCircuit
from qiskit.aqua import AquaError
from qiskit.aqua.utils import CircuitFactory
from qiskit.aqua.circuits import PhaseEstimationCircuit
Expand All @@ -36,26 +35,36 @@


class AmplitudeEstimation(AmplitudeEstimationAlgorithm):
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.

Can we rename this to be QPEAmplitudeEstimation, and say in the comment on line 38 that this is "The original, Quantum Phase Estimation-based Amplitude Estimation Algorithm."? I think it may be strange to have one algorithm called AmplitudeEstimation but then a base class called AmplitudeEstimationAlgorithm. It would be better to be descriptive. If so, we should also rename the file qpe_ae.py, yes?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The alternative might be to rename the base class, which would be less disruptive - otherwise notebooks, test cases and any users code would need to alter too. I do not know how much, or if at all, externally that base class type might be used at present outside of the package. I suspect it could be renamed without the same impact as the algorithm derived from it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AmplitudeEstimation is used quite a bit in notebooks (and our codes), I also prefer to keep it. As discussed on Slack I'll update the the docstring though.

"""
The Amplitude Estimation algorithm.
r"""The Quantum Phase Estimation-based Amplitude Estimation algorithm.

This class implements the original Quantum Amplitude Estimation (QAE) algorithm, introduced by
https://arxiv.org/abs/quant-ph/0005055. This (original) version uses quantum phase
estimation along with a set of m ancilla qubits to find an estimate, that is restricted
to the grid

\{sin^2(\pi y / 2^m) : y = 0, ..., 2^{m-1}\}.

Using a maximum likelihood post processing, this grid constraint can be circumvented.
This improved estimator is implemented as well, see https://arxiv.org/abs/1912.05559 Appendix A
for more detail.
"""

def __init__(self, num_eval_qubits: int,
a_factory: Optional[CircuitFactory] = None,
i_objective: Optional[int] = None,
q_factory: Optional[CircuitFactory] = None,
i_objective: Optional[int] = None,
iqft: Optional[IQFT] = None) -> None:
"""
r"""Initializer.

Args:
num_eval_qubits: number of evaluation qubits, has a min. value of 1.
a_factory: the CircuitFactory subclass object representing
the problem unitary
i_objective: i objective
q_factory: the CircuitFactory subclass object representing an
amplitude estimation sample (based on a_factory)
iqft: the Inverse Quantum Fourier Transform component,
defaults to using a standard iqft when None
num_eval_qubits: Number of evaluation qubits, has a min. value of 1.
a_factory: The CircuitFactory subclass object representing the problem unitary.
q_factory: The CircuitFactory subclass object representing an amplitude estimation
sample (based on a_factory).
i_objective: The index of the objective qubit, i.e. the qubit marking 'good' solutions
with the state \|1> and 'bad' solutions with the state \|0>.
iqft: the Inverse Quantum Fourier Transform component, defaults to using a standard IQFT
when None
"""
validate_min('num_eval_qubits', num_eval_qubits, 1)
super().__init__(a_factory, q_factory, i_objective)
Expand All @@ -72,7 +81,12 @@ def __init__(self, num_eval_qubits: int,
self._ret = {}

@property
def _num_qubits(self):
def _num_qubits(self) -> int:
"""Return the number of qubits needed in the circuit.

Returns:
The total number of qubits.
"""
if self.a_factory is None: # if A factory is not set, no qubits are specified
return 0

Expand All @@ -81,16 +95,14 @@ def _num_qubits(self):

return num_qubits

def construct_circuit(self, measurement=False):
"""
Construct the Amplitude Estimation quantum circuit.
def construct_circuit(self, measurement: bool = False) -> QuantumCircuit:
"""Construct the Amplitude Estimation quantum circuit.

Args:
measurement (bool): Boolean flag to indicate if measurement
should be included in the circuit.
measurement: Boolean flag to indicate if measurements should be included in the circuit.

Returns:
QuantumCircuit: the QuantumCircuit object for the constructed circuit
The QuantumCircuit object for the constructed circuit.
"""
pec = PhaseEstimationCircuit(
iqft=self._iqft, num_ancillae=self._m,
Expand All @@ -101,7 +113,21 @@ def construct_circuit(self, measurement=False):
self._circuit = pec.construct_circuit(measurement=measurement)
return self._circuit

def _evaluate_statevector_results(self, probabilities):
def _evaluate_statevector_results(self, probabilities: Union[List[float], np.ndarray]
) -> Tuple[OrderedDict, OrderedDict]:
"""Evaluate the results from statevector simulation.

Given the probabilities from statevector simulation of the QAE circuit, compute the
probabilities that the measurements y/gridpoints a are the best estimate.

Args:
probabilities: The probabilities obtained from the statevector simulation,
i.e. real(statevector * statevector.conj())[0]

Returns:
Dictionaries containing the a gridpoints with respective probabilities and
y measurements with respective probabilities, in this order.
"""
# map measured results to estimates
y_probabilities = OrderedDict()
for i, probability in enumerate(probabilities):
Expand All @@ -113,16 +139,27 @@ def _evaluate_statevector_results(self, probabilities):
for y, probability in y_probabilities.items():
if y >= int(self._M / 2):
y = self._M - y
# due to the finite accuracy of the sine, we round the result to 7 decimals
a = np.round(np.power(np.sin(y * np.pi / 2 ** self._m), 2),
decimals=7)
a_probabilities[a] = a_probabilities.get(a, 0) + probability

return a_probabilities, y_probabilities

def _compute_fisher_information(self, observed=False):
def _compute_fisher_information(self, observed: bool = False) -> float:
"""Computes the Fisher information for the output of the previous run.

Args:
observed: If True, the observed Fisher information is returned, otherwise
the expected Fisher information.

Returns:
The Fisher information.
"""
fisher_information = None
mlv = self._ret['ml_value'] # MLE in [0,1]
m = self._m

if observed:
ai = np.asarray(self._ret['values'])
pi = np.asarray(self._ret['probabilities'])
Expand All @@ -139,25 +176,54 @@ def integrand(x):

return fisher_information

def _fisher_ci(self, alpha, observed=False):
def _fisher_confint(self, alpha: float, observed: bool = False) -> List[float]:
"""Compute the Fisher information confidence interval for the MLE of the previous run.

Args:
alpha: Specifies the (1 - alpha) confidence level (0 < alpha < 1).
observed: If True, the observed Fisher information is used to construct the
confidence interval, otherwise the expected Fisher information.

Returns:
The Fisher information confidence interval.
"""
shots = self._ret['shots']
mle = self._ret['ml_value']

# approximate the standard deviation of the MLE and construct the confidence interval
std = np.sqrt(shots * self._compute_fisher_information(observed))
ci = mle + norm.ppf(1 - alpha / 2) / std * np.array([-1, 1])

# transform the confidence interval from [0, 1] to the target interval
return [self.a_factory.value_to_estimation(bound) for bound in ci]

def _likelihood_ratio_ci(self, alpha):
def _likelihood_ratio_confint(self, alpha: float) -> List[float]:
"""Compute the likelihood ratio confidence interval for the MLE of the previous run.

Args:
alpha: Specifies the (1 - alpha) confidence level (0 < alpha < 1).

Returns:
The likelihood ratio confidence interval.
"""
# Compute the two intervals in which we the look for values above
# the likelihood ratio: the two bubbles next to the QAE estimate
M = 2**self._m
qae = self._ret['value']
y = M * np.arcsin(np.sqrt(qae)) / np.pi
left_of_qae = np.sin(np.pi * (y - 1) / M)**2
right_of_qae = np.sin(np.pi * (y + 1) / M)**2

bubbles = [left_of_qae, qae, right_of_qae]
y = int(np.round(M * np.arcsin(np.sqrt(qae)) / np.pi))
if y == 0:
right_of_qae = np.sin(np.pi * (y + 1) / M)**2
bubbles = [qae, right_of_qae]

elif y == int(M / 2): # remember, M = 2^m is a power of 2
left_of_qae = np.sin(np.pi * (y - 1) / M)**2
bubbles = [left_of_qae, qae]

else:
left_of_qae = np.sin(np.pi * (y - 1) / M)**2
right_of_qae = np.sin(np.pi * (y + 1) / M)**2
bubbles = [left_of_qae, qae, right_of_qae]

# likelihood function
ai = np.asarray(self._ret['values'])
Expand All @@ -177,6 +243,9 @@ def cut(x):
return loglikelihood(x) - thres

# Store the boundaries of the confidence interval
# It's valid to start off with the zero-width confidence interval, since the maximum
# of the likelihood function is guaranteed to be over the threshold, and if alpha = 0
# that's the valid interval
lower = upper = self._ret['ml_value']

# Check the two intervals/bubbles: check if they surpass the
Expand All @@ -199,49 +268,48 @@ def cut(x):
ci = [lower, upper]
return [self.a_factory.value_to_estimation(bound) for bound in ci]

def confidence_interval(self, alpha, kind='likelihood_ratio'):
"""
Compute the (1 - alpha) confidence interval
def confidence_interval(self, alpha: float, kind: str = 'likelihood_ratio') -> List[float]:
"""Compute the (1 - alpha) confidence interval.

Args:
alpha (float): confidence level: compute the (1 - alpha) confidence interval
kind (str): the method to compute the confidence interval, can be 'fisher',
'observed_fisher' or 'likelihood_ratio' (default)
alpha: Confidence level: compute the (1 - alpha) confidence interval.
kind: The method to compute the confidence interval, can be 'fisher', 'observed_fisher'
or 'likelihood_ratio' (default)

Returns:
list[float]: the (1 - alpha) confidence interval
The (1 - alpha) confidence interval of the specified kind.

Raises:
AquaError: if 'mle' is not in self._ret.keys() (i.e. `run` was not called yet)
NotImplementedError: if the confidence interval method `kind` is not implemented
AquaError: If 'mle' is not in self._ret.keys() (i.e. `run` was not called yet).
NotImplementedError: If the confidence interval method `kind` is not implemented.
"""
# check if AE did run already
if 'mle' not in self._ret.keys():
raise AquaError('Call run() first!')

# if statevector simulator the estimate is exact
if self._quantum_instance.is_statevector:
return 2 * [self._ret['estimation']]
return 2 * [self._ret['mle']]

if kind in ['likelihood_ratio', 'lr']:
return self._likelihood_ratio_ci(alpha)
return self._likelihood_ratio_confint(alpha)

if kind in ['fisher', 'fi']:
return self._fisher_ci(alpha, observed=False)
return self._fisher_confint(alpha, observed=False)

if kind in ['observed_fisher', 'observed_information', 'oi']:
return self._fisher_ci(alpha, observed=True)
return self._fisher_confint(alpha, observed=True)

raise NotImplementedError('CI `{}` is not implemented.'.format(kind))

def _run_mle(self):
"""
Compute the Maximum Likelihood Estimator (MLE)
def _run_mle(self) -> None:
"""Compute the Maximum Likelihood Estimator (MLE).

Returns:
The MLE for the previous AE run
The MLE for the previous AE run.

Note: Before calling this method, call the method `run` of the AmplitudeEstimation instance
Note:
Before calling this method, call the method `run` of the AmplitudeEstimation instance.
"""
M = self._M
qae = self._ret['value']
Expand All @@ -262,12 +330,11 @@ def loglikelihood(a):
# Compute the two intervals in which are candidates for containing
# the maximum of the log-likelihood function: the two bubbles next to
# the QAE estimate
bubbles = None
if y == 0:
right_of_qae = np.sin(np.pi * (y + 1) / M)**2
bubbles = [qae, right_of_qae]

elif y == int(M / 2):
elif y == int(M / 2): # remember, M = 2^m is a power of 2
left_of_qae = np.sin(np.pi * (y - 1) / M)**2
bubbles = [left_of_qae, qae]

Expand All @@ -292,7 +359,7 @@ def loglikelihood(a):
self._ret['ml_value'] = a_opt
self._ret['mle'] = val_opt

def _run(self):
def _run(self) -> dict:
# check if A factory has been set
if self.a_factory is None:
raise AquaError("a_factory must be set!")
Expand Down Expand Up @@ -326,7 +393,8 @@ def _run(self):
# construct probabilities
y_probabilities = {}
a_probabilities = {}
shots = sum(ret.get_counts().values())
shots = self._quantum_instance._run_config.shots

for state, counts in ret.get_counts().items():
y = int(state.replace(' ', '')[:self._m][::-1], 2)
p = counts / shots
Expand Down Expand Up @@ -368,6 +436,9 @@ def _run(self):
self._ret['estimation'] = est
self._ret['value'] = val

# count the number of Q-oracle calls
self._ret['num_oracle_queries'] = self._M - 1

# get MLE
self._run_mle()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,17 @@ def i_objective(self):
return self.a_factory.num_target_qubits - 1

return None

@i_objective.setter
def i_objective(self, i_objective):
"""
Set the index of the objective qubit, i.e. the qubit deciding between 'good/bad' states.

Args:
i_objective (int): the index

Note:
No checks about the validity of the index are performed, since i_objective could also
be set before the A/Q operators and in that case checks cannot be done.
"""
self._i_objective = i_objective
Loading