Source code for qiskit_algorithms.amplitude_estimators.iae

# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2018, 2024.
#
# 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.

"""The Iterative Quantum Amplitude Estimation Algorithm."""

from __future__ import annotations
from typing import cast, Callable, Tuple
import warnings
import numpy as np
from scipy.stats import beta

from qiskit import ClassicalRegister, QuantumCircuit
from qiskit.primitives import BaseSampler, Sampler

from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult
from .estimation_problem import EstimationProblem
from ..exceptions import AlgorithmError


[docs]class IterativeAmplitudeEstimation(AmplitudeEstimator): r"""The Iterative Amplitude Estimation algorithm. This class implements the Iterative Quantum Amplitude Estimation (IQAE) algorithm, proposed in [1]. The output of the algorithm is an estimate that, with at least probability :math:`1 - \alpha`, differs by epsilon to the target value, where both alpha and epsilon can be specified. It differs from the original QAE algorithm proposed by Brassard [2] in that it does not rely on Quantum Phase Estimation, but is only based on Grover's algorithm. IQAE iteratively applies carefully selected Grover iterations to find an estimate for the target amplitude. References: [1]: Grinko, D., Gacon, J., Zoufal, C., & Woerner, S. (2019). Iterative Quantum Amplitude Estimation. `arXiv:1912.05559 <https://arxiv.org/abs/1912.05559>`_. [2]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). Quantum Amplitude Amplification and Estimation. `arXiv:quant-ph/0005055 <http://arxiv.org/abs/quant-ph/0005055>`_. """ # pylint: disable=too-many-positional-arguments def __init__( self, epsilon_target: float, alpha: float, confint_method: str = "beta", min_ratio: float = 2, sampler: BaseSampler | None = None, ) -> None: r""" The output of the algorithm is an estimate for the amplitude `a`, that with at least probability 1 - alpha has an error of epsilon. The number of A operator calls scales linearly in 1/epsilon (up to a logarithmic factor). Args: epsilon_target: Target precision for estimation target `a`, has values between 0 and 0.5 alpha: Confidence level, the target probability is 1 - alpha, has values between 0 and 1 confint_method: Statistical method used to estimate the confidence intervals in each iteration, can be 'chernoff' for the Chernoff intervals or 'beta' for the Clopper-Pearson intervals (default) min_ratio: Minimal q-ratio (:math:`K_{i+1} / K_i`) for FindNextK sampler: A sampler primitive to evaluate the circuits. Raises: AlgorithmError: if the method to compute the confidence intervals is not supported ValueError: If the target epsilon is not in (0, 0.5] ValueError: If alpha is not in (0, 1) ValueError: If confint_method is not supported """ # validate ranges of input arguments if not 0 < epsilon_target <= 0.5: raise ValueError(f"The target epsilon must be in (0, 0.5], but is {epsilon_target}.") if not 0 < alpha < 1: raise ValueError(f"The confidence level alpha must be in (0, 1), but is {alpha}") if confint_method not in {"chernoff", "beta"}: raise ValueError( f"The confidence interval method must be chernoff or beta, but is {confint_method}." ) super().__init__() # store parameters self._epsilon = epsilon_target self._alpha = alpha self._min_ratio = min_ratio self._confint_method = confint_method self._sampler = sampler @property def sampler(self) -> BaseSampler | None: """Get the sampler primitive. Returns: The sampler primitive to evaluate the circuits. """ return self._sampler @sampler.setter def sampler(self, sampler: BaseSampler) -> None: """Set sampler primitive. Args: sampler: A sampler primitive to evaluate the circuits. """ self._sampler = sampler @property def epsilon_target(self) -> float: """Returns the target precision ``epsilon_target`` of the algorithm. Returns: The target precision (which is half the width of the confidence interval). """ return self._epsilon @epsilon_target.setter def epsilon_target(self, epsilon: float) -> None: """Set the target precision of the algorithm. Args: epsilon: Target precision for estimation target `a`. """ self._epsilon = epsilon def _find_next_k( self, k: int, upper_half_circle: bool, theta_interval: tuple[float, float], min_ratio: float = 2.0, ) -> tuple[int, bool]: """Find the largest integer k_next, such that the interval (4 * k_next + 2)*theta_interval lies completely in [0, pi] or [pi, 2pi], for theta_interval = (theta_lower, theta_upper). Args: k: The current power of the Q operator. upper_half_circle: Boolean flag of whether theta_interval lies in the upper half-circle [0, pi] or in the lower one [pi, 2pi]. theta_interval: The current confidence interval for the angle theta, i.e. (theta_lower, theta_upper). min_ratio: Minimal ratio K/K_next allowed in the algorithm. Returns: The next power k, and boolean flag for the extrapolated interval. Raises: AlgorithmError: if min_ratio is smaller or equal to 1 """ if min_ratio <= 1: raise AlgorithmError("min_ratio must be larger than 1 to ensure convergence") # initialize variables theta_l, theta_u = theta_interval old_scaling = 4 * k + 2 # current scaling factor, called K := (4k + 2) # the largest feasible scaling factor K cannot be larger than K_max, # which is bounded by the length of the current confidence interval max_scaling = int(1 / (2 * (theta_u - theta_l))) scaling = max_scaling - (max_scaling - 2) % 4 # bring into the form 4 * k_max + 2 # find the largest feasible scaling factor K_next, and thus k_next while scaling >= min_ratio * old_scaling: theta_min = scaling * theta_l - int(scaling * theta_l) theta_max = scaling * theta_u - int(scaling * theta_u) if theta_min <= theta_max <= 0.5 and theta_min <= 0.5: # the extrapolated theta interval is in the upper half-circle upper_half_circle = True return int((scaling - 2) / 4), upper_half_circle elif theta_max >= 0.5 and theta_max >= theta_min >= 0.5: # the extrapolated theta interval is in the upper half-circle upper_half_circle = False return int((scaling - 2) / 4), upper_half_circle scaling -= 4 # if we do not find a feasible k, return the old one return int(k), upper_half_circle
[docs] def construct_circuit( self, estimation_problem: EstimationProblem, k: int = 0, measurement: bool = False ) -> QuantumCircuit: r"""Construct the circuit :math:`\mathcal{Q}^k \mathcal{A} |0\rangle`. The A operator is the unitary specifying the QAE problem and Q the associated Grover operator. Args: estimation_problem: The estimation problem for which to construct the QAE circuit. k: The power of the Q operator. measurement: Boolean flag to indicate if measurements should be included in the circuits. Returns: The circuit implementing :math:`\mathcal{Q}^k \mathcal{A} |0\rangle`. """ num_qubits = max( estimation_problem.state_preparation.num_qubits, estimation_problem.grover_operator.num_qubits, ) circuit = QuantumCircuit(num_qubits, name="circuit") # add classical register if needed if measurement: c = ClassicalRegister(len(estimation_problem.objective_qubits)) circuit.add_register(c) # add A operator circuit.compose(estimation_problem.state_preparation, inplace=True) # add Q^k if k != 0: circuit.compose(estimation_problem.grover_operator.power(k), inplace=True) # add optional measurement if measurement: # real hardware can currently not handle operations after measurements, which might # happen if the circuit gets transpiled, hence we're adding a safeguard-barrier circuit.barrier() circuit.measure(estimation_problem.objective_qubits, c[:]) return circuit
def _good_state_probability( self, problem: EstimationProblem, counts_dict: dict[str, int], ) -> tuple[int, float]: """Get the probability to measure '1' in the last qubit. Args: problem: The estimation problem, used to obtain the number of objective qubits and the ``is_good_state`` function. counts_dict: A counts-dictionary (with one measured qubit only!) Returns: #one-counts, #one-counts/#all-counts """ one_counts = 0 for state, counts in counts_dict.items(): if problem.is_good_state(state): one_counts += counts return int(one_counts), one_counts / sum(counts_dict.values())
[docs] def estimate( self, estimation_problem: EstimationProblem ) -> "IterativeAmplitudeEstimationResult": """Run the amplitude estimation algorithm on provided estimation problem. Args: estimation_problem: The estimation problem. Returns: An amplitude estimation results object. Raises: ValueError: A Sampler must be provided. AlgorithmError: Sampler job run error. """ if self._sampler is None: warnings.warn("No sampler provided, defaulting to Sampler from qiskit.primitives") self._sampler = Sampler() # initialize memory variables powers = [0] # list of powers k: Q^k, (called 'k' in paper) ratios = [] # list of multiplication factors (called 'q' in paper) theta_intervals = [[0, 1 / 4]] # a priori knowledge of theta / 2 / pi a_intervals = [[0.0, 1.0]] # a priori knowledge of the confidence interval of the estimate num_oracle_queries = 0 num_one_shots = [] # maximum number of rounds max_rounds = ( int(np.log(self._min_ratio * np.pi / 8 / self._epsilon) / np.log(self._min_ratio)) + 1 ) upper_half_circle = True # initially theta is in the upper half-circle num_iterations = 0 # keep track of the number of iterations # do while loop, keep in mind that we scaled theta mod 2pi such that it lies in [0,1] while theta_intervals[-1][1] - theta_intervals[-1][0] > self._epsilon / np.pi: num_iterations += 1 # get the next k k, upper_half_circle = self._find_next_k( powers[-1], upper_half_circle, theta_intervals[-1], # type: ignore min_ratio=self._min_ratio, ) # store the variables powers.append(k) ratios.append((2 * powers[-1] + 1) / (2 * powers[-2] + 1)) # run measurements for Q^k A|0> circuit circuit = self.construct_circuit(estimation_problem, k, measurement=True) counts = {} try: job = self._sampler.run([circuit]) ret = job.result() except Exception as exc: raise AlgorithmError("The job was not completed successfully. ") from exc shots = ret.metadata[0].get("shots") if shots is None: circuit = self.construct_circuit(estimation_problem, k=0, measurement=True) try: job = self._sampler.run([circuit]) ret = job.result() except Exception as exc: raise AlgorithmError("The job was not completed successfully. ") from exc # calculate the probability of measuring '1' prob = 0.0 for bit, probabilities in ret.quasi_dists[0].binary_probabilities().items(): # check if it is a good state if estimation_problem.is_good_state(bit): prob += probabilities a_confidence_interval = [prob, prob] a_intervals.append(a_confidence_interval) theta_i_interval = [ np.arccos(1 - 2 * a_i) / 2 / np.pi for a_i in a_confidence_interval ] theta_intervals.append(theta_i_interval) num_oracle_queries = 0 # no Q-oracle call, only a single one to A break counts = { k: round(v * shots) for k, v in ret.quasi_dists[0].binary_probabilities().items() } # calculate the probability of measuring '1', 'prob' is a_i in the paper one_counts, prob = self._good_state_probability(estimation_problem, counts) num_one_shots.append(one_counts) # track number of Q-oracle calls num_oracle_queries += shots * k # if on the previous iterations we have K_{i-1} == K_i, we sum these samples up j = 1 # number of times we stayed fixed at the same K round_shots = shots round_one_counts = one_counts if num_iterations > 1: while ( powers[num_iterations - j] == powers[num_iterations] and num_iterations >= j + 1 ): j = j + 1 round_shots += shots round_one_counts += num_one_shots[-j] # compute a_min_i, a_max_i if self._confint_method == "chernoff": a_i_min, a_i_max = _chernoff_confint(prob, round_shots, max_rounds, self._alpha) else: # 'beta' a_i_min, a_i_max = _clopper_pearson_confint( round_one_counts, round_shots, self._alpha / max_rounds ) # compute theta_min_i, theta_max_i if upper_half_circle: theta_min_i = np.arccos(1 - 2 * a_i_min) / 2 / np.pi theta_max_i = np.arccos(1 - 2 * a_i_max) / 2 / np.pi else: theta_min_i = 1 - np.arccos(1 - 2 * a_i_max) / 2 / np.pi theta_max_i = 1 - np.arccos(1 - 2 * a_i_min) / 2 / np.pi # compute theta_u, theta_l of this iteration scaling = 4 * k + 2 # current K_i factor theta_u = (int(scaling * theta_intervals[-1][1]) + theta_max_i) / scaling theta_l = (int(scaling * theta_intervals[-1][0]) + theta_min_i) / scaling theta_intervals.append([theta_l, theta_u]) # compute a_u_i, a_l_i a_u = np.sin(2 * np.pi * theta_u) ** 2 a_l = np.sin(2 * np.pi * theta_l) ** 2 a_u = cast(float, a_u) a_l = cast(float, a_l) a_intervals.append([a_l, a_u]) # get the latest confidence interval for the estimate of a confidence_interval = cast(Tuple[float, float], a_intervals[-1]) # the final estimate is the mean of the confidence interval estimation = np.mean(confidence_interval) result = IterativeAmplitudeEstimationResult() result.alpha = self._alpha result.post_processing = cast(Callable[[float], float], estimation_problem.post_processing) result.num_oracle_queries = num_oracle_queries result.estimation = float(estimation) result.epsilon_estimated = (confidence_interval[1] - confidence_interval[0]) / 2 result.confidence_interval = confidence_interval result.estimation_processed = estimation_problem.post_processing( estimation # type: ignore[arg-type,assignment] ) confidence_interval = tuple( estimation_problem.post_processing(x) # type: ignore[arg-type,assignment] for x in confidence_interval ) result.confidence_interval_processed = confidence_interval result.epsilon_estimated_processed = (confidence_interval[1] - confidence_interval[0]) / 2 result.estimate_intervals = a_intervals result.theta_intervals = theta_intervals result.powers = powers result.ratios = ratios return result
[docs]class IterativeAmplitudeEstimationResult(AmplitudeEstimatorResult): """The ``IterativeAmplitudeEstimation`` result object.""" def __init__(self) -> None: super().__init__() self._alpha: float | None = None self._epsilon_target: float | None = None self._epsilon_estimated: float | None = None self._epsilon_estimated_processed: float | None = None self._estimate_intervals: list[list[float]] | None = None self._theta_intervals: list[list[float]] | None = None self._powers: list[int] | None = None self._ratios: list[float] | None = None self._confidence_interval_processed: tuple[float, float] | None = None @property def alpha(self) -> float: r"""Return the confidence level :math:`\alpha`.""" return self._alpha @alpha.setter def alpha(self, value: float) -> None: r"""Set the confidence level :math:`\alpha`.""" self._alpha = value @property def epsilon_target(self) -> float: """Return the target half-width of the confidence interval.""" return self._epsilon_target @epsilon_target.setter def epsilon_target(self, value: float) -> None: """Set the target half-width of the confidence interval.""" self._epsilon_target = value @property def epsilon_estimated(self) -> float: """Return the estimated half-width of the confidence interval.""" return self._epsilon_estimated @epsilon_estimated.setter def epsilon_estimated(self, value: float) -> None: """Set the estimated half-width of the confidence interval.""" self._epsilon_estimated = value @property def epsilon_estimated_processed(self) -> float: """Return the post-processed estimated half-width of the confidence interval.""" return self._epsilon_estimated_processed @epsilon_estimated_processed.setter def epsilon_estimated_processed(self, value: float) -> None: """Set the post-processed estimated half-width of the confidence interval.""" self._epsilon_estimated_processed = value @property def estimate_intervals(self) -> list[list[float]]: """Return the confidence intervals for the estimate in each iteration.""" return self._estimate_intervals @estimate_intervals.setter def estimate_intervals(self, value: list[list[float]]) -> None: """Set the confidence intervals for the estimate in each iteration.""" self._estimate_intervals = value @property def theta_intervals(self) -> list[list[float]]: """Return the confidence intervals for the angles in each iteration.""" return self._theta_intervals @theta_intervals.setter def theta_intervals(self, value: list[list[float]]) -> None: """Set the confidence intervals for the angles in each iteration.""" self._theta_intervals = value @property def powers(self) -> list[int]: """Return the powers of the Grover operator in each iteration.""" return self._powers @powers.setter def powers(self, value: list[int]) -> None: """Set the powers of the Grover operator in each iteration.""" self._powers = value @property def ratios(self) -> list[float]: r"""Return the ratios :math:`K_{i+1}/K_{i}` for each iteration :math:`i`.""" return self._ratios @ratios.setter def ratios(self, value: list[float]) -> None: r"""Set the ratios :math:`K_{i+1}/K_{i}` for each iteration :math:`i`.""" self._ratios = value @property def confidence_interval_processed(self) -> tuple[float, float]: """Return the post-processed confidence interval.""" return self._confidence_interval_processed @confidence_interval_processed.setter def confidence_interval_processed(self, value: tuple[float, float]) -> None: """Set the post-processed confidence interval.""" self._confidence_interval_processed = value
def _chernoff_confint( value: float, shots: int, max_rounds: int, alpha: float ) -> tuple[float, float]: """Compute the Chernoff confidence interval for `shots` i.i.d. Bernoulli trials. The confidence interval is [value - eps, value + eps], where eps = sqrt(3 * log(2 * max_rounds/ alpha) / shots) but at most [0, 1]. Args: value: The current estimate. shots: The number of shots. max_rounds: The maximum number of rounds, used to compute epsilon_a. alpha: The confidence level, used to compute epsilon_a. Returns: The Chernoff confidence interval. """ eps = np.sqrt(3 * np.log(2 * max_rounds / alpha) / shots) lower = np.maximum(0, value - eps) upper = np.minimum(1, value + eps) return lower, upper def _clopper_pearson_confint(counts: int, shots: int, alpha: float) -> tuple[float, float]: """Compute the Clopper-Pearson confidence interval for `shots` i.i.d. Bernoulli trials. Args: counts: The number of positive counts. shots: The number of shots. alpha: The confidence level for the confidence interval. Returns: The Clopper-Pearson confidence interval. """ lower, upper = 0, 1 # if counts == 0, the beta quantile returns nan if counts != 0: lower = beta.ppf(alpha / 2, counts, shots - counts + 1) # if counts == shots, the beta quantile returns nan if counts != shots: upper = beta.ppf(1 - alpha / 2, counts + 1, shots - counts) return lower, upper