diff --git a/qiskit/algorithms/amplitude_estimators/ae.py b/qiskit/algorithms/amplitude_estimators/ae.py index 4d4c89873235..512405f19c9f 100644 --- a/qiskit/algorithms/amplitude_estimators/ae.py +++ b/qiskit/algorithms/amplitude_estimators/ae.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -12,18 +12,22 @@ """The Quantum Phase Estimation-based Amplitude Estimation algorithm.""" -from typing import Optional, Union, List, Tuple, Dict +from __future__ import annotations from collections import OrderedDict +import warnings import numpy as np from scipy.stats import chi2, norm from scipy.optimize import bisect from qiskit import QuantumCircuit, ClassicalRegister from qiskit.providers import Backend +from qiskit.primitives import BaseSampler from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult from .ae_utils import pdf_a, derivative_log_pdf_a, bisect_max from .estimation_problem import EstimationProblem +from ..exceptions import AlgorithmError class AmplitudeEstimation(AmplitudeEstimator): @@ -56,9 +60,10 @@ class AmplitudeEstimation(AmplitudeEstimator): def __init__( self, num_eval_qubits: int, - phase_estimation_circuit: Optional[QuantumCircuit] = None, - iqft: Optional[QuantumCircuit] = None, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, + phase_estimation_circuit: QuantumCircuit | None = None, + iqft: QuantumCircuit | None = None, + quantum_instance: QuantumInstance | Backend | None = None, + sampler: BaseSampler | None = None, ) -> None: r""" Args: @@ -68,7 +73,9 @@ def __init__( `qiskit.circuit.library.PhaseEstimation` when None. iqft: The inverse quantum Fourier transform component, defaults to using a standard implementation from `qiskit.circuit.library.QFT` when None. - quantum_instance: The backend (or `QuantumInstance`) to execute the circuits on. + quantum_instance: Pending deprecation\: The backend (or `QuantumInstance`) to execute + the circuits on. + sampler: A sampler primitive to evaluate the circuits. Raises: ValueError: If the number of evaluation qubits is smaller than 1. @@ -79,7 +86,16 @@ def __init__( super().__init__() # set quantum instance - self.quantum_instance = quantum_instance + if quantum_instance is not None: + warnings.warn( + "The quantum_instance argument has been superseded by the sampler argument. " + "This argument will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.quantum_instance = quantum_instance # get parameters self._m = num_eval_qubits # pylint: disable=invalid-name @@ -87,10 +103,35 @@ def __init__( self._iqft = iqft self._pec = phase_estimation_circuit + 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 quantum_instance(self) -> Optional[QuantumInstance]: - """Get the quantum instance. + @deprecate_function( + "The AmplitudeEstimation.quantum_instance getter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self) -> QuantumInstance | None: + """Pending deprecation; Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -98,8 +139,14 @@ def quantum_instance(self) -> Optional[QuantumInstance]: return self._quantum_instance @quantum_instance.setter - def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: - """Set quantum instance. + @deprecate_function( + "The AmplitudeEstimation.quantum_instance setter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: + """Pending deprecation; Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. @@ -149,9 +196,9 @@ def construct_circuit( def evaluate_measurements( self, - circuit_results: Union[Dict[str, int], np.ndarray], + circuit_results: dict[str, int] | np.ndarray, threshold: float = 1e-6, - ) -> Tuple[Dict[int, float], Dict[float, float]]: + ) -> tuple[dict[int, float], dict[float, float]]: """Evaluate the results from the circuit simulation. Given the probabilities from statevector simulation of the QAE circuit, compute the @@ -159,7 +206,7 @@ def evaluate_measurements( Args: circuit_results: The circuit result from the QAE circuit. Can be either a counts dict - or a statevector. + or a statevector or a quasi-probabilities dict. threshold: Measurements with probabilities below the threshold are discarded. Returns: @@ -168,7 +215,10 @@ def evaluate_measurements( """ # compute grid sample and measurement dicts if isinstance(circuit_results, dict): - samples, measurements = self._evaluate_count_results(circuit_results) + if set(map(type, circuit_results.values())) == {int}: + samples, measurements = self._evaluate_count_results(circuit_results) + else: + samples, measurements = self._evaluate_quasi_probabilities_results(circuit_results) else: samples, measurements = self._evaluate_statevector_results(circuit_results) @@ -197,12 +247,24 @@ def _evaluate_statevector_results(self, statevector): return samples, measurements - def _evaluate_count_results(self, counts): + def _evaluate_quasi_probabilities_results(self, circuit_results): # construct probabilities measurements = OrderedDict() samples = OrderedDict() - shots = self._quantum_instance._run_config.shots + for state, probability in circuit_results.items(): + # reverts the last _m items + y = int(state[: -self._m - 1 : -1], 2) + measurements[y] = probability + a = np.round(np.power(np.sin(y * np.pi / 2**self._m), 2), decimals=7) + samples[a] = samples.get(a, 0.0) + probability + + return samples, measurements + def _evaluate_count_results(self, counts): + # construct probabilities + measurements = OrderedDict() + samples = OrderedDict() + shots = sum(counts.values()) for state, count in counts.items(): y = int(state.replace(" ", "")[: self._m][::-1], 2) probability = count / shots @@ -283,12 +345,16 @@ def estimate(self, estimation_problem: EstimationProblem) -> "AmplitudeEstimatio Raises: ValueError: If `state_preparation` or `objective_qubits` are not set in the `estimation_problem`. + ValueError: A quantum instance or sampler must be provided. + AlgorithmError: Sampler job run error. """ # check if A factory or state_preparation has been set if estimation_problem.state_preparation is None: raise ValueError( "The state_preparation property of the estimation problem must be set." ) + if self._quantum_instance is None and self._sampler is None: + raise ValueError("A quantum instance or sampler must be provided.") if estimation_problem.objective_qubits is None: raise ValueError("The objective_qubits property of the estimation problem must be set.") @@ -297,24 +363,43 @@ def estimate(self, estimation_problem: EstimationProblem) -> "AmplitudeEstimatio result.num_evaluation_qubits = self._m result.post_processing = estimation_problem.post_processing - if self._quantum_instance.is_statevector: + shots = 0 + if self._quantum_instance is not None and self._quantum_instance.is_statevector: circuit = self.construct_circuit(estimation_problem, measurement=False) # run circuit on statevector simulator statevector = self._quantum_instance.execute(circuit).get_statevector() result.circuit_results = statevector - # store number of shots: convention is 1 shot for statevector, # needed so that MLE works! - result.shots = 1 + shots = 1 else: - # run circuit on QASM simulator circuit = self.construct_circuit(estimation_problem, measurement=True) - counts = self._quantum_instance.execute(circuit).get_counts() - result.circuit_results = counts - - # store shots - result.shots = sum(counts.values()) - + if self._quantum_instance is not None: + # run circuit on QASM simulator + result.circuit_results = self._quantum_instance.execute(circuit).get_counts() + shots = sum(result.circuit_results.values()) + else: + 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: + result.circuit_results = { + np.binary_repr(k, circuit.num_qubits): v + for k, v in ret.quasi_dists[0].items() + } + shots = 1 + else: + result.circuit_results = { + np.binary_repr(k, circuit.num_qubits): round(v * shots) + for k, v in ret.quasi_dists[0].items() + } + + # store shots + result.shots = shots samples, measurements = self.evaluate_measurements(result.circuit_results) result.samples = samples @@ -349,7 +434,7 @@ def estimate(self, estimation_problem: EstimationProblem) -> "AmplitudeEstimatio @staticmethod def compute_confidence_interval( result: "AmplitudeEstimationResult", alpha: float = 0.05, kind: str = "likelihood_ratio" - ) -> Tuple[float, float]: + ) -> tuple[float, float]: """Compute the (1 - alpha) confidence interval. Args: @@ -415,12 +500,12 @@ def mle_processed(self, value: float) -> None: self._mle_processed = value @property - def samples_processed(self) -> Dict[float, float]: + def samples_processed(self) -> dict[float, float]: """Return the post-processed measurement samples with their measurement probability.""" return self._samples_processed @samples_processed.setter - def samples_processed(self, value: Dict[float, float]) -> None: + def samples_processed(self, value: dict[float, float]) -> None: """Set the post-processed measurement samples.""" self._samples_processed = value @@ -435,22 +520,22 @@ def mle(self, value: float) -> None: self._mle = value @property - def samples(self) -> Dict[float, float]: + def samples(self) -> dict[float, float]: """Return the measurement samples with their measurement probability.""" return self._samples @samples.setter - def samples(self, value: Dict[float, float]) -> None: + def samples(self, value: dict[float, float]) -> None: """Set the measurement samples with their measurement probability.""" self._samples = value @property - def measurements(self) -> Dict[int, float]: + def measurements(self) -> dict[int, float]: """Return the measurements as integers with their measurement probability.""" return self._y_measurements @measurements.setter - def measurements(self, value: Dict[int, float]) -> None: + def measurements(self, value: dict[int, float]) -> None: """Set the measurements as integers with their measurement probability.""" self._y_measurements = value @@ -500,7 +585,7 @@ def integrand(x): def _fisher_confint( result: AmplitudeEstimationResult, alpha: float, observed: bool = False -) -> List[float]: +) -> list[float]: """Compute the Fisher information confidence interval for the MLE of the previous run. Args: @@ -520,7 +605,7 @@ def _fisher_confint( return tuple(result.post_processing(bound) for bound in confint) -def _likelihood_ratio_confint(result: AmplitudeEstimationResult, alpha: float) -> List[float]: +def _likelihood_ratio_confint(result: AmplitudeEstimationResult, alpha: float) -> list[float]: """Compute the likelihood ratio confidence interval for the MLE of the previous run. Args: diff --git a/qiskit/algorithms/amplitude_estimators/ae_utils.py b/qiskit/algorithms/amplitude_estimators/ae_utils.py index bd695be45a63..87ce83a9cc42 100644 --- a/qiskit/algorithms/amplitude_estimators/ae_utils.py +++ b/qiskit/algorithms/amplitude_estimators/ae_utils.py @@ -20,6 +20,22 @@ # pylint: disable=invalid-name +def _probabilities_from_sampler_result(num_qubits, result, estimation_problem): + """calculate probabilities from sampler result""" + prob = 0 + for bit, probabilities in result.quasi_dists[0].items(): + i = int(bit) + # get bitstring of objective qubits + full_state = bin(i)[2:].zfill(num_qubits)[::-1] + state = "".join([full_state[i] for i in estimation_problem.objective_qubits]) + + # check if it is a good state + if estimation_problem.is_good_state(state[::-1]): + prob += probabilities + + return prob + + def bisect_max(f, a, b, steps=50, minwidth=1e-12, retval=False): """Find the maximum of the real-valued function f in the interval [a, b] using bisection. diff --git a/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py b/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py index 93136b034737..eb2e5a7f4f67 100644 --- a/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py +++ b/qiskit/algorithms/amplitude_estimators/amplitude_estimator.py @@ -12,8 +12,9 @@ """The Amplitude Estimation interface.""" +from __future__ import annotations from abc import abstractmethod, ABC -from typing import Union, Optional, Dict, Callable, Tuple +from typing import Callable import numpy as np from .estimation_problem import EstimationProblem @@ -49,12 +50,12 @@ def __init__(self) -> None: self._confidence_interval_processed = None @property - def circuit_results(self) -> Optional[Union[np.ndarray, Dict[str, int]]]: + def circuit_results(self) -> np.ndarray | dict[str, int] | None: """Return the circuit results. Can be a statevector or counts dictionary.""" return self._circuit_results @circuit_results.setter - def circuit_results(self, value: Union[np.ndarray, Dict[str, int]]) -> None: + def circuit_results(self, value: np.ndarray | dict[str, int]) -> None: """Set the circuit results.""" self._circuit_results = value @@ -109,21 +110,21 @@ def post_processing(self, post_processing: Callable[[float], float]) -> None: self._post_processing = post_processing @property - def confidence_interval(self) -> Tuple[float, float]: + def confidence_interval(self) -> tuple[float, float]: """Return the confidence interval for the amplitude (95% interval by default).""" return self._confidence_interval @confidence_interval.setter - def confidence_interval(self, confidence_interval: Tuple[float, float]) -> None: + def confidence_interval(self, confidence_interval: tuple[float, float]) -> None: """Set the confidence interval for the amplitude (95% interval by default).""" self._confidence_interval = confidence_interval @property - def confidence_interval_processed(self) -> Tuple[float, float]: + def confidence_interval_processed(self) -> tuple[float, float]: """Return the post-processed confidence interval (95% interval by default).""" return self._confidence_interval_processed @confidence_interval_processed.setter - def confidence_interval_processed(self, confidence_interval: Tuple[float, float]) -> None: + def confidence_interval_processed(self, confidence_interval: tuple[float, float]) -> None: """Set the post-processed confidence interval (95% interval by default).""" self._confidence_interval_processed = confidence_interval diff --git a/qiskit/algorithms/amplitude_estimators/estimation_problem.py b/qiskit/algorithms/amplitude_estimators/estimation_problem.py index be563614809b..d38c3c0e2f3c 100644 --- a/qiskit/algorithms/amplitude_estimators/estimation_problem.py +++ b/qiskit/algorithms/amplitude_estimators/estimation_problem.py @@ -12,8 +12,9 @@ """The Estimation problem class.""" +from __future__ import annotations import warnings -from typing import Optional, List, Callable, Union +from typing import Callable import numpy from qiskit.circuit import QuantumCircuit, QuantumRegister @@ -32,10 +33,10 @@ class EstimationProblem: def __init__( self, state_preparation: QuantumCircuit, - objective_qubits: Union[int, List[int]], - grover_operator: Optional[QuantumCircuit] = None, - post_processing: Optional[Callable[[float], float]] = None, - is_good_state: Optional[Callable[[str], bool]] = None, + objective_qubits: int | list[int], + grover_operator: QuantumCircuit | None = None, + post_processing: Callable[[float], float] | None = None, + is_good_state: Callable[[str], bool] | None = None, ) -> None: r""" Args: @@ -59,7 +60,7 @@ def __init__( self._is_good_state = is_good_state @property - def state_preparation(self) -> Optional[QuantumCircuit]: + def state_preparation(self) -> QuantumCircuit | None: r"""Get the :math:`\mathcal{A}` operator encoding the amplitude :math:`a`. Returns: @@ -77,7 +78,7 @@ def state_preparation(self, state_preparation: QuantumCircuit) -> None: self._state_preparation = state_preparation @property - def objective_qubits(self) -> List[int]: + def objective_qubits(self) -> list[int]: """Get the criterion for a measurement outcome to be in a 'good' state. Returns: @@ -89,7 +90,7 @@ def objective_qubits(self) -> List[int]: return self._objective_qubits @objective_qubits.setter - def objective_qubits(self, objective_qubits: Union[int, List[int]]) -> None: + def objective_qubits(self, objective_qubits: int | list[int]) -> None: """Set the criterion for a measurement outcome to be in a 'good' state. Args: @@ -110,7 +111,7 @@ def post_processing(self) -> Callable[[float], float]: return self._post_processing @post_processing.setter - def post_processing(self, post_processing: Optional[Callable[[float], float]]) -> None: + def post_processing(self, post_processing: Callable[[float], float] | None) -> None: """Set the post processing function. Args: @@ -132,7 +133,7 @@ def is_good_state(self) -> Callable[[str], bool]: return self._is_good_state @is_good_state.setter - def is_good_state(self, is_good_state: Optional[Callable[[str], bool]]) -> None: + def is_good_state(self, is_good_state: Callable[[str], bool] | None) -> None: """Set the ``is_good_state`` function. Args: @@ -142,7 +143,7 @@ def is_good_state(self, is_good_state: Optional[Callable[[str], bool]]) -> None: self._is_good_state = is_good_state @property - def grover_operator(self) -> Optional[QuantumCircuit]: + def grover_operator(self) -> QuantumCircuit | None: r"""Get the :math:`\mathcal{Q}` operator, or Grover operator. If the Grover operator is not set, we try to build it from the :math:`\mathcal{A}` operator @@ -172,7 +173,7 @@ def grover_operator(self) -> Optional[QuantumCircuit]: return GroverOperator(oracle, self.state_preparation) @grover_operator.setter - def grover_operator(self, grover_operator: Optional[QuantumCircuit]) -> None: + def grover_operator(self, grover_operator: QuantumCircuit | None) -> None: r"""Set the :math:`\mathcal{Q}` operator. Args: diff --git a/qiskit/algorithms/amplitude_estimators/fae.py b/qiskit/algorithms/amplitude_estimators/fae.py index db8ee9151d50..697556840df0 100644 --- a/qiskit/algorithms/amplitude_estimators/fae.py +++ b/qiskit/algorithms/amplitude_estimators/fae.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (C) Copyright IBM 2017, 2022. # # 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 @@ -12,16 +12,20 @@ """Faster Amplitude Estimation.""" -from typing import Optional, Union, List, Tuple +from __future__ import annotations +import warnings import numpy as np from qiskit.circuit import QuantumCircuit, ClassicalRegister from qiskit.providers import Backend +from qiskit.primitives import BaseSampler from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function from qiskit.algorithms.exceptions import AlgorithmError from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult from .estimation_problem import EstimationProblem +from .ae_utils import _probabilities_from_sampler_result class FasterAmplitudeEstimation(AmplitudeEstimator): @@ -50,14 +54,17 @@ def __init__( delta: float, maxiter: int, rescale: bool = True, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, + quantum_instance: QuantumInstance | Backend | None = None, + sampler: BaseSampler | None = None, ) -> None: r""" Args: delta: The probability that the true value is outside of the final confidence interval. maxiter: The number of iterations, the maximal power of Q is `2 ** (maxiter - 1)`. rescale: Whether to rescale the problem passed to `estimate`. - quantum_instance: The quantum instance or backend to run the circuits. + quantum_instance: Pending deprecation\: The quantum instance or backend + to run the circuits. + sampler: A sampler primitive to evaluate the circuits. .. note:: @@ -66,16 +73,51 @@ def __init__( """ super().__init__() - self.quantum_instance = quantum_instance + # set quantum instance + if quantum_instance is not None: + warnings.warn( + "The quantum_instance argument has been superseded by the sampler argument. " + "This argument will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.quantum_instance = quantum_instance self._shots = (int(1944 * np.log(2 / delta)), int(972 * np.log(2 / delta))) self._rescale = rescale self._delta = delta self._maxiter = maxiter self._num_oracle_calls = 0 + self._sampler = sampler @property - def quantum_instance(self) -> Optional[QuantumInstance]: - """Get the quantum instance. + 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 + @deprecate_function( + "The FasterAmplitudeEstimation.quantum_instance getter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self) -> QuantumInstance | None: + """Pending deprecation; Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -83,8 +125,14 @@ def quantum_instance(self) -> Optional[QuantumInstance]: return self._quantum_instance @quantum_instance.setter - def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: - """Set quantum instance. + @deprecate_function( + "The FasterAmplitudeEstimation.quantum_instance setter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: + """Pending deprecation; Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. @@ -94,10 +142,26 @@ def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> self._quantum_instance = quantum_instance def _cos_estimate(self, estimation_problem, k, shots): - if self._quantum_instance is None: - raise AlgorithmError("Quantum instance must be set.") + if self._quantum_instance is None and self._sampler is None: + raise ValueError("A quantum instance or sampler must be provided.") - if self._quantum_instance.is_statevector: + if self._sampler is not None: + circuit = self.construct_circuit(estimation_problem, k, measurement=True) + try: + job = self._sampler.run([circuit], shots=shots) + result = job.result() + except Exception as exc: + raise AlgorithmError("The job was not completed successfully. ") from exc + + if shots is None: + shots = 1 + self._num_oracle_calls += (2 * k + 1) * shots + # sum over all probabilities where the objective qubits are 1 + prob = _probabilities_from_sampler_result( + circuit.num_qubits, result, estimation_problem + ) + cos_estimate = 1 - 2 * prob + elif self._quantum_instance.is_statevector: circuit = self.construct_circuit(estimation_problem, k, measurement=False) statevector = self._quantum_instance.execute(circuit).get_statevector() @@ -136,7 +200,7 @@ def _chernoff(self, cos, shots): def construct_circuit( self, estimation_problem: EstimationProblem, k: int, measurement: bool = False - ) -> Union[QuantumCircuit, Tuple[QuantumCircuit, List[int]]]: + ) -> QuantumCircuit | tuple[QuantumCircuit, list[int]]: r"""Construct the circuit :math:`Q^k X |0\rangle>`. The A operator is the unitary specifying the QAE problem and Q the associated Grover @@ -179,21 +243,37 @@ def construct_circuit( return circuit def estimate(self, estimation_problem: EstimationProblem) -> "FasterAmplitudeEstimationResult": + """Run the amplitude estimation algorithm on provided estimation problem. + + Args: + estimation_problem: The estimation problem. + + Returns: + An amplitude estimation results object. + + Raises: + ValueError: A quantum instance or Sampler must be provided. + AlgorithmError: Sampler run error. + """ + if self._quantum_instance is None and self._sampler is None: + raise ValueError("A quantum instance or sampler must be provided.") + self._num_oracle_calls = 0 - user_defined_shots = self.quantum_instance._run_config.shots + user_defined_shots = ( + self._quantum_instance._run_config.shots if self._quantum_instance is not None else None + ) if self._rescale: problem = estimation_problem.rescale(0.25) else: problem = estimation_problem - if self._quantum_instance.is_statevector: + if self._quantum_instance is not None and self._quantum_instance.is_statevector: cos = self._cos_estimate(problem, k=0, shots=1) theta = np.arccos(cos) / 2 theta_ci = [theta, theta] theta_cis = [theta_ci] num_steps = num_first_stage_steps = 1 - else: theta_ci = [0, np.arcsin(0.25)] first_stage = True @@ -240,7 +320,7 @@ def cos_estimate(power, shots): result.num_oracle_queries = self._num_oracle_calls result.num_steps = num_steps result.num_first_state_steps = num_first_stage_steps - if self._quantum_instance.is_statevector: + if self._quantum_instance is not None and self._quantum_instance.is_statevector: result.success_probability = 1 else: result.success_probability = 1 - (2 * self._maxiter - j_0) * self._delta @@ -252,7 +332,9 @@ def cos_estimate(power, shots): result.theta_intervals = theta_cis # reset shots to what the user had defined - self.quantum_instance._run_config.shots = user_defined_shots + if self._quantum_instance is not None: + self._quantum_instance._run_config.shots = user_defined_shots + return result @@ -297,11 +379,11 @@ def num_first_state_steps(self, num_steps: int) -> None: self._num_first_state_steps = num_steps @property - def theta_intervals(self) -> List[List[float]]: + 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: + def theta_intervals(self, value: list[list[float]]) -> None: """Set the confidence intervals for the angles in each iteration.""" self._theta_intervals = value diff --git a/qiskit/algorithms/amplitude_estimators/iae.py b/qiskit/algorithms/amplitude_estimators/iae.py index 42727cc5aea0..59a4429d0bf4 100644 --- a/qiskit/algorithms/amplitude_estimators/iae.py +++ b/qiskit/algorithms/amplitude_estimators/iae.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -12,16 +12,21 @@ """The Iterative Quantum Amplitude Estimation Algorithm.""" -from typing import Optional, Union, List, Tuple, Dict, cast +from __future__ import annotations +from typing import cast +import warnings import numpy as np from scipy.stats import beta from qiskit import ClassicalRegister, QuantumCircuit from qiskit.providers import Backend +from qiskit.primitives import BaseSampler from qiskit.utils import QuantumInstance +from qiskit.utils.deprecation import deprecate_function from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult from .estimation_problem import EstimationProblem +from .ae_utils import _probabilities_from_sampler_result from ..exceptions import AlgorithmError @@ -52,7 +57,8 @@ def __init__( alpha: float, confint_method: str = "beta", min_ratio: float = 2, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, + quantum_instance: QuantumInstance | Backend | None = None, + sampler: BaseSampler | None = None, ) -> None: r""" The output of the algorithm is an estimate for the amplitude `a`, that with at least @@ -66,7 +72,8 @@ def __init__( 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 - quantum_instance: Quantum Instance or Backend + quantum_instance: Pending deprecation\: Quantum Instance or Backend + sampler: A sampler primitive to evaluate the circuits. Raises: AlgorithmError: if the method to compute the confidence intervals is not supported @@ -89,17 +96,51 @@ def __init__( super().__init__() # set quantum instance - self.quantum_instance = quantum_instance + if quantum_instance is not None: + warnings.warn( + "The quantum_instance argument has been superseded by the sampler argument. " + "This argument will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.quantum_instance = quantum_instance # store parameters self._epsilon = epsilon_target self._alpha = alpha self._min_ratio = min_ratio self._confint_method = confint_method + self._sampler = sampler @property - def quantum_instance(self) -> Optional[QuantumInstance]: - """Get the quantum instance. + 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 + @deprecate_function( + "The IterativeAmplitudeEstimation.quantum_instance getter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self) -> QuantumInstance | None: + """Pending deprecation; Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -107,8 +148,14 @@ def quantum_instance(self) -> Optional[QuantumInstance]: return self._quantum_instance @quantum_instance.setter - def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: - """Set quantum instance. + @deprecate_function( + "The IterativeAmplitudeEstimation.quantum_instance setter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: + """Pending deprecation; Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. @@ -139,9 +186,9 @@ def _find_next_k( self, k: int, upper_half_circle: bool, - theta_interval: Tuple[float, float], + theta_interval: tuple[float, float], min_ratio: float = 2.0, - ) -> Tuple[int, bool]: + ) -> 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). @@ -238,9 +285,9 @@ def construct_circuit( def _good_state_probability( self, problem: EstimationProblem, - counts_or_statevector: Union[Dict[str, int], np.ndarray], + counts_or_statevector: dict[str, int] | np.ndarray, num_state_qubits: int, - ) -> Union[Tuple[int, float], float]: + ) -> tuple[int, float] | float: """Get the probability to measure '1' in the last qubit. Args: @@ -279,6 +326,21 @@ def _good_state_probability( 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 quantum instance or Sampler must be provided. + AlgorithmError: Sampler job run error. + """ + if self._quantum_instance is None and self._sampler is None: + raise ValueError("A quantum instance or sampler must be provided.") + # initialize memory variables powers = [0] # list of powers k: Q^k, (called 'k' in paper) ratios = [] # list of multiplication factors (called 'q' in paper) @@ -293,9 +355,9 @@ def estimate( ) upper_half_circle = True # initially theta is in the upper half-circle - # for statevector we can directly return the probability to measure 1 - # note, that no iterations here are necessary - if self._quantum_instance.is_statevector: + if self._quantum_instance is not None and self._quantum_instance.is_statevector: + # for statevector we can directly return the probability to measure 1 + # note, that no iterations here are necessary # simulate circuit circuit = self.construct_circuit(estimation_problem, k=0, measurement=False) ret = self._quantum_instance.execute(circuit) @@ -308,7 +370,7 @@ def estimate( prob = self._good_state_probability(estimation_problem, statevector, num_qubits) prob = cast(float, prob) # tell MyPy it's a float and not Tuple[int, float ] - a_confidence_interval = [prob, prob] # type: List[float] + a_confidence_interval = [prob, prob] # type: list[float] a_intervals.append(a_confidence_interval) theta_i_interval = [ @@ -319,8 +381,8 @@ def estimate( else: num_iterations = 0 # keep track of the number of iterations - shots = self._quantum_instance._run_config.shots # number of shots per iteration - + # number of shots per iteration + shots = 0 # 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 @@ -339,10 +401,52 @@ def estimate( # run measurements for Q^k A|0> circuit circuit = self.construct_circuit(estimation_problem, k, measurement=True) - ret = self._quantum_instance.execute(circuit) - - # get the counts and store them - counts = ret.get_counts(circuit) + counts = {} + if self._quantum_instance is not None: + ret = self._quantum_instance.execute(circuit) + # get the counts and store them + counts = ret.get_counts(circuit) + shots = self._quantum_instance._run_config.shots + else: + 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 = _probabilities_from_sampler_result( + circuit.num_qubits, ret, estimation_problem + ) + prob = cast( + float, prob + ) # tell MyPy it's a float and not Tuple[int, float ] + + a_confidence_interval = [prob, prob] # type: list[float] + 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 = { + np.binary_repr(k, circuit.num_qubits): round(v * shots) + for k, v in ret.quasi_dists[0].items() + } # calculate the probability of measuring '1', 'prob' is a_i in the paper num_qubits = circuit.num_qubits - circuit.num_ancillas @@ -483,59 +587,59 @@ def epsilon_estimated_processed(self, value: float) -> None: self._epsilon_estimated_processed = value @property - def estimate_intervals(self) -> List[List[float]]: + 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: + 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]]: + 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: + 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]: + 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: + 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]: + 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: + 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]: + 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: + 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]: +) -> tuple[float, float]: """Compute the Chernoff confidence interval for `shots` i.i.d. Bernoulli trials. The confidence interval is @@ -559,7 +663,7 @@ def _chernoff_confint( return lower, upper -def _clopper_pearson_confint(counts: int, shots: int, alpha: float) -> Tuple[float, float]: +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: diff --git a/qiskit/algorithms/amplitude_estimators/mlae.py b/qiskit/algorithms/amplitude_estimators/mlae.py index 21ca0cb35d1a..8d46e1a2c21e 100644 --- a/qiskit/algorithms/amplitude_estimators/mlae.py +++ b/qiskit/algorithms/amplitude_estimators/mlae.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -12,7 +12,9 @@ """The Maximum Likelihood Amplitude Estimation algorithm.""" -from typing import Optional, List, Union, Tuple, Dict, Callable +from __future__ import annotations +import typing +import warnings import numpy as np from scipy.optimize import brute from scipy.stats import norm, chi2 @@ -20,12 +22,16 @@ from qiskit.providers import Backend from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit from qiskit.utils import QuantumInstance +from qiskit.primitives import BaseSampler +from qiskit.utils.deprecation import deprecate_function from .amplitude_estimator import AmplitudeEstimator, AmplitudeEstimatorResult from .estimation_problem import EstimationProblem from ..exceptions import AlgorithmError -MINIMIZER = Callable[[Callable[[float], float], List[Tuple[float, float]]], float] +MINIMIZER = typing.Callable[ + [typing.Callable[[float], float], typing.List[typing.Tuple[float, float]]], float +] class MaximumLikelihoodAmplitudeEstimation(AmplitudeEstimator): @@ -49,9 +55,10 @@ class in named ``MaximumLikelihoodAmplitudeEstimation``. def __init__( self, - evaluation_schedule: Union[List[int], int], - minimizer: Optional[MINIMIZER] = None, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, + evaluation_schedule: list[int] | int, + minimizer: MINIMIZER | None = None, + quantum_instance: QuantumInstance | Backend | None = None, + sampler: BaseSampler | None = None, ) -> None: r""" Args: @@ -64,7 +71,8 @@ def __init__( according to ``evaluation_schedule``. The minimizer takes a function as first argument and a list of (float, float) tuples (as bounds) as second argument and returns a single float which is the found minimum. - quantum_instance: Quantum Instance or Backend + quantum_instance: Pending deprecation\: Quantum Instance or Backend + sampler: A sampler primitive to evaluate the circuits. Raises: ValueError: If the number of oracle circuits is smaller than 1. @@ -73,7 +81,16 @@ def __init__( super().__init__() # set quantum instance - self.quantum_instance = quantum_instance + if quantum_instance is not None: + warnings.warn( + "The quantum_instance argument has been superseded by the sampler argument. " + "This argument will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.quantum_instance = quantum_instance # get parameters if isinstance(evaluation_schedule, int): @@ -98,9 +115,35 @@ def default_minimizer(objective_fn, bounds): else: self._minimizer = minimizer + 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 quantum_instance(self) -> Optional[QuantumInstance]: - """Get the quantum instance. + @deprecate_function( + "The MaximumLikelihoodAmplitudeEstimation.quantum_instance getter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self) -> QuantumInstance | None: + """Pending deprecation; Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -108,8 +151,14 @@ def quantum_instance(self) -> Optional[QuantumInstance]: return self._quantum_instance @quantum_instance.setter - def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> None: - """Set quantum instance. + @deprecate_function( + "The MaximumLikelihoodAmplitudeEstimation.quantum_instance setter is pending deprecation. " + "This property will be deprecated in a future release and subsequently " + "removed after that.", + category=PendingDeprecationWarning, + ) + def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: + """Pending deprecation; Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. @@ -120,7 +169,7 @@ def quantum_instance(self, quantum_instance: Union[QuantumInstance, Backend]) -> def construct_circuits( self, estimation_problem: EstimationProblem, measurement: bool = False - ) -> List[QuantumCircuit]: + ) -> list[QuantumCircuit]: """Construct the Amplitude Estimation w/o QPE quantum circuits. Args: @@ -148,7 +197,7 @@ def construct_circuits( qc_0.compose(estimation_problem.state_preparation, inplace=True) for k in self._evaluation_schedule: - qc_k = qc_0.copy(name="qc_a_q_%s" % k) + qc_k = qc_0.copy(name=f"qc_a_q_{k}") if k != 0: qc_k.compose(estimation_problem.grover_operator.power(k), inplace=True) @@ -170,7 +219,7 @@ def compute_confidence_interval( alpha: float, kind: str = "fisher", apply_post_processing: bool = False, - ) -> Tuple[float, float]: + ) -> tuple[float, float]: """Compute the `alpha` confidence interval using the method `kind`. The confidence level is (1 - `alpha`) and supported kinds are 'fisher', @@ -216,11 +265,11 @@ def compute_confidence_interval( def compute_mle( self, - circuit_results: Union[List[Dict[str, int]], List[np.ndarray]], + circuit_results: list[dict[str, int]] | list[np.ndarray], estimation_problem: EstimationProblem, - num_state_qubits: Optional[int] = None, + num_state_qubits: int | None = None, return_counts: bool = False, - ) -> Union[float, Tuple[float, List[float]]]: + ) -> float | tuple[float, list[float]]: """Compute the MLE via a grid-search. This is a stable approach if sufficient gridpoints are used. @@ -259,10 +308,25 @@ def loglikelihood(theta): def estimate( self, estimation_problem: EstimationProblem ) -> "MaximumLikelihoodAmplitudeEstimationResult": + """Run the amplitude estimation algorithm on provided estimation problem. + + Args: + estimation_problem: The estimation problem. + + Returns: + An amplitude estimation results object. + + Raises: + ValueError: A quantum instance or Sampler must be provided. + AlgorithmError: If `state_preparation` is not set in + `estimation_problem`. + AlgorithmError: Sampler job run error + """ + if self._quantum_instance is None and self._sampler is None: + raise ValueError("A quantum instance or sampler must be provided.") if estimation_problem.state_preparation is None: raise AlgorithmError( - "Either the state_preparation variable or the a_factory " - "(deprecated) must be set to run the algorithm." + "The state_preparation property of the estimation problem must be set." ) result = MaximumLikelihoodAmplitudeEstimationResult() @@ -270,7 +334,8 @@ def estimate( result.minimizer = self._minimizer result.post_processing = estimation_problem.post_processing - if self._quantum_instance.is_statevector: + shots = 0 + if self._quantum_instance is not None and self._quantum_instance.is_statevector: # run circuit on statevector simulator circuits = self.construct_circuits(estimation_problem, measurement=False) ret = self._quantum_instance.execute(circuits) @@ -281,17 +346,41 @@ def estimate( # to count the number of Q-oracle calls (don't count shots) result.shots = 1 - else: - # run circuit on QASM simulator circuits = self.construct_circuits(estimation_problem, measurement=True) - ret = self._quantum_instance.execute(circuits) - - # get counts and construct MLE input - result.circuit_results = [ret.get_counts(circuit) for circuit in circuits] - - # to count the number of Q-oracle calls - result.shots = self._quantum_instance._run_config.shots + if self._quantum_instance is not None: + # run circuit on QASM simulator + ret = self._quantum_instance.execute(circuits) + # get counts and construct MLE input + result.circuit_results = [ret.get_counts(circuit) for circuit in circuits] + shots = self._quantum_instance._run_config.shots + else: + try: + job = self._sampler.run(circuits) + ret = job.result() + except Exception as exc: + raise AlgorithmError("The job was not completed successfully. ") from exc + + result.circuit_results = [] + shots = ret.metadata[0].get("shots") + if shots is None: + for i, quasi_dist in enumerate(ret.quasi_dists): + circuit_result = { + np.binary_repr(k, circuits[i].num_qubits): v + for k, v in quasi_dist.items() + } + result.circuit_results.append(circuit_result) + shots = 1 + else: + # get counts and construct MLE input + for circuit in circuits: + counts = { + np.binary_repr(k, circuit.num_qubits): round(v * shots) + for k, v in ret.quasi_dists[0].items() + } + result.circuit_results.append(counts) + + result.shots = shots # run maximum likelihood estimation num_state_qubits = circuits[0].num_qubits - circuits[0].num_ancillas @@ -353,22 +442,22 @@ def minimizer(self, value: callable) -> None: self._minimizer = value @property - def good_counts(self) -> List[float]: + def good_counts(self) -> list[float]: """Return the percentage of good counts per circuit power.""" return self._good_counts @good_counts.setter - def good_counts(self, counts: List[float]) -> None: + def good_counts(self, counts: list[float]) -> None: """Set the percentage of good counts per circuit power.""" self._good_counts = counts @property - def evaluation_schedule(self) -> List[int]: + def evaluation_schedule(self) -> list[int]: """Return the evaluation schedule for the powers of the Grover operator.""" return self._evaluation_schedule @evaluation_schedule.setter - def evaluation_schedule(self, evaluation_schedule: List[int]) -> None: + def evaluation_schedule(self, evaluation_schedule: list[int]) -> None: """Set the evaluation schedule for the powers of the Grover operator.""" self._evaluation_schedule = evaluation_schedule @@ -397,7 +486,7 @@ def _safe_max(array, default=(np.pi / 2)): def _compute_fisher_information( result: "MaximumLikelihoodAmplitudeEstimationResult", - num_sum_terms: Optional[int] = None, + num_sum_terms: int | None = None, observed: bool = False, ) -> float: """Compute the Fisher information. @@ -455,7 +544,7 @@ def _compute_fisher_information( def _fisher_confint( result: MaximumLikelihoodAmplitudeEstimationResult, alpha: float = 0.05, observed: bool = False -) -> Tuple[float, float]: +) -> tuple[float, float]: """Compute the `alpha` confidence interval based on the Fisher information. Args: @@ -489,8 +578,8 @@ def _fisher_confint( def _likelihood_ratio_confint( result: MaximumLikelihoodAmplitudeEstimationResult, alpha: float = 0.05, - nevals: Optional[int] = None, -) -> List[float]: + nevals: int | None = None, +) -> list[float]: """Compute the likelihood-ratio confidence interval. Args: @@ -538,10 +627,10 @@ def loglikelihood(theta, one_counts, all_counts): def _get_counts( - circuit_results: List[Union[np.ndarray, List[float], Dict[str, int]]], + circuit_results: list[np.ndarray | list[float], dict[str, int]], estimation_problem: EstimationProblem, num_state_qubits: int, -) -> Tuple[List[float], List[int]]: +) -> tuple[list[float], list[int]]: """Get the good and total counts. Returns: diff --git a/releasenotes/notes/ae-algorithms-primitives-497bae1b2b04f877.yaml b/releasenotes/notes/ae-algorithms-primitives-497bae1b2b04f877.yaml new file mode 100644 index 000000000000..35e204ef1f60 --- /dev/null +++ b/releasenotes/notes/ae-algorithms-primitives-497bae1b2b04f877.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added :class:`~qiskit.primitives.BaseSampler` as ``init`` parameter + for the following amplitude estimation algorithms: + :class:`~qiskit.algorithms.amplitude_estimators.AmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.FasterAmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.IterativeAmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.MaximumLikelihoodAmplitudeEstimation` +deprecations: + - | + Using :class:`~qiskit.utils.QuantumInstance` or :class:`~qiskit.providers.Backend` as + ``init`` parameters will now issue a ``PendingDeprecationWarning`` + for the following amplitude estimation algorithms: + :class:`~qiskit.algorithms.amplitude_estimators.AmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.FasterAmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.IterativeAmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.MaximumLikelihoodAmplitudeEstimation` diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py index 0b41e863aed2..8360ff4fdd91 100644 --- a/test/python/algorithms/test_amplitude_estimators.py +++ b/test/python/algorithms/test_amplitude_estimators.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -27,6 +27,7 @@ EstimationProblem, ) from qiskit.quantum_info import Operator, Statevector +from qiskit.primitives import Sampler class BernoulliStateIn(QuantumCircuit): @@ -94,12 +95,8 @@ def setUp(self): seed_simulator=2, seed_transpiler=2, ) - self._unitary = QuantumInstance( - backend=BasicAer.get_backend("unitary_simulator"), - shots=1, - seed_simulator=42, - seed_transpiler=91, - ) + + self._sampler = Sampler(options={"seed": 2}) def qasm(shots=100): return QuantumInstance( @@ -111,6 +108,11 @@ def qasm(shots=100): self._qasm = qasm + def sampler_shots(shots=100): + return Sampler(options={"shots": shots, "seed": 2}) + + self._sampler_shots = sampler_shots + @idata( [ [0.2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2}], @@ -136,6 +138,30 @@ def test_statevector(self, prob, qae, expect): value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" ) + @idata( + [ + [0.2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.2}], + [0.49, AmplitudeEstimation(3), {"estimation": 0.5, "mle": 0.49}], + [0.2, MaximumLikelihoodAmplitudeEstimation([0, 1, 2]), {"estimation": 0.2}], + [0.49, MaximumLikelihoodAmplitudeEstimation(3), {"estimation": 0.49}], + [0.2, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.2}], + [0.49, IterativeAmplitudeEstimation(0.001, 0.01), {"estimation": 0.49}], + [0.2, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.199}], + [0.12, FasterAmplitudeEstimation(0.1, 2, rescale=False), {"estimation": 0.12}], + ] + ) + @unpack + def test_sampler(self, prob, qae, expect): + """sampler test""" + qae.sampler = self._sampler + problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) + + result = qae.estimate(problem) + for key, value in expect.items(): + self.assertAlmostEqual( + value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" + ) + @idata( [ [0.2, 100, AmplitudeEstimation(4), {"estimation": 0.14644, "mle": 0.193888}], @@ -168,6 +194,38 @@ def test_qasm(self, prob, shots, qae, expect): value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" ) + @idata( + [ + [0.2, 100, AmplitudeEstimation(4), {"estimation": 0.500000, "mle": 0.562783}], + [0.0, 1000, AmplitudeEstimation(2), {"estimation": 0.0, "mle": 0.0}], + [ + 0.2, + 100, + MaximumLikelihoodAmplitudeEstimation([0, 1, 2, 4, 8]), + {"estimation": 0.474790}, + ], + [0.8, 10, IterativeAmplitudeEstimation(0.1, 0.05), {"estimation": 0.811711}], + [0.2, 1000, FasterAmplitudeEstimation(0.1, 3, rescale=False), {"estimation": 0.199073}], + [ + 0.12, + 100, + FasterAmplitudeEstimation(0.01, 3, rescale=False), + {"estimation": 0.120016}, + ], + ] + ) + @unpack + def test_sampler_with_shots(self, prob, shots, qae, expect): + """sampler with shots test""" + qae.sampler = self._sampler_shots(shots) + problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) + + result = qae.estimate(problem) + for key, value in expect.items(): + self.assertAlmostEqual( + value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" + ) + @data(True, False) def test_qae_circuit(self, efficient_circuit): """Test circuits resulting from canonical amplitude estimation. @@ -321,6 +379,8 @@ def setUp(self): seed_transpiler=41, ) + self._sampler = Sampler(options={"seed": 123}) + def qasm(shots=100): return QuantumInstance( backend=BasicAer.get_backend("qasm_simulator"), @@ -331,6 +391,11 @@ def qasm(shots=100): self._qasm = qasm + def sampler_shots(shots=100): + return Sampler(options={"shots": shots, "seed": 7192}) + + self._sampler_shots = sampler_shots + @idata( [ [2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.270290}], @@ -355,6 +420,28 @@ def test_statevector(self, n, qae, expect): value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" ) + @idata( + [ + [2, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.270290}], + [4, MaximumLikelihoodAmplitudeEstimation(4), {"estimation": 0.0}], + [3, IterativeAmplitudeEstimation(0.1, 0.1), {"estimation": 0.0}], + [3, FasterAmplitudeEstimation(0.01, 1), {"estimation": 0.017687}], + ] + ) + @unpack + def test_sampler(self, n, qae, expect): + """sampler end-to-end test""" + # construct factories for A and Q + # qae.state_preparation = SineIntegral(n) + qae.sampler = self._sampler + estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + + result = qae.estimate(estimation_problem) + for key, value in expect.items(): + self.assertAlmostEqual( + value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" + ) + @idata( [ [4, 10, AmplitudeEstimation(2), {"estimation": 0.5, "mle": 0.333333}], @@ -376,6 +463,27 @@ def test_qasm(self, n, shots, qae, expect): value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" ) + @idata( + [ + [4, 10, AmplitudeEstimation(2), {"estimation": 0.0, "mle": 0.0}], + [3, 10, MaximumLikelihoodAmplitudeEstimation(2), {"estimation": 0.0}], + [3, 1000, IterativeAmplitudeEstimation(0.01, 0.01), {"estimation": 0.0}], + [3, 1000, FasterAmplitudeEstimation(0.1, 4), {"estimation": 0.000551}], + ] + ) + @unpack + def test_sampler_with_shots(self, n, shots, qae, expect): + """Sampler with shots end-to-end test.""" + # construct factories for A and Q + qae.sampler = self._sampler_shots(shots) + estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + + result = qae.estimate(estimation_problem) + for key, value in expect.items(): + self.assertAlmostEqual( + value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" + ) + @idata( [ [ @@ -454,6 +562,10 @@ def test_iqae_confidence_intervals(self): class TestFasterAmplitudeEstimation(QiskitAlgorithmsTestCase): """Specific tests for Faster AE.""" + def setUp(self): + super().setUp() + self._sampler = Sampler(options={"seed": 2}) + def test_rescaling(self): """Test the rescaling.""" amplitude = 0.8 @@ -490,6 +602,28 @@ def test_run_without_rescaling(self): value_without_scaling = np.sin(theta) ** 2 self.assertAlmostEqual(result.estimation, value_without_scaling) + def test_sampler_run_without_rescaling(self): + """Run Faster AE without rescaling if the amplitude is in [0, 1/4].""" + # construct estimation problem + prob = 0.11 + a_op = QuantumCircuit(1) + a_op.ry(2 * np.arcsin(np.sqrt(prob)), 0) + problem = EstimationProblem(a_op, objective_qubits=[0]) + + # construct algo without rescaling + fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, sampler=self._sampler) + + # run the algo + result = fae.estimate(problem) + + # assert the result is correct + self.assertAlmostEqual(result.estimation, prob, places=2) + + # assert no rescaling was used + theta = np.mean(result.theta_intervals[-1]) + value_without_scaling = np.sin(theta) ** 2 + self.assertAlmostEqual(result.estimation, value_without_scaling) + def test_rescaling_with_custom_grover_raises(self): """Test that the rescaling option fails if a custom Grover operator is used.""" prob = 0.8