From 72d7c251c3fcd267db8c30622e65ff93f2b65f39 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 12 Aug 2022 15:52:18 +0900 Subject: [PATCH 01/37] added the gradients with the primitives Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/base_estimator_gradient.py | 78 ++++ .../gradients/base_sampler_gradient.py | 71 ++++ .../gradients/estimator_gradient_job.py | 36 ++ .../finite_diff_estimator_gradient.py | 126 +++++++ .../gradients/finite_diff_sampler_gradient.py | 137 +++++++ .../gradients/lin_comb_estimator_gradient.py | 126 +++++++ .../gradients/lin_comb_sampler_gradient.py | 120 ++++++ .../param_shift_estimator_gradient.py | 150 ++++++++ .../gradients/param_shift_sampler_gradient.py | 160 ++++++++ .../gradients/sampler_gradient_job.py | 37 ++ qiskit/algorithms/gradients/utils.py | 343 ++++++++++++++++++ 11 files changed, 1384 insertions(+) create mode 100644 qiskit/algorithms/gradients/base_estimator_gradient.py create mode 100644 qiskit/algorithms/gradients/base_sampler_gradient.py create mode 100644 qiskit/algorithms/gradients/estimator_gradient_job.py create mode 100644 qiskit/algorithms/gradients/finite_diff_estimator_gradient.py create mode 100644 qiskit/algorithms/gradients/finite_diff_sampler_gradient.py create mode 100644 qiskit/algorithms/gradients/lin_comb_estimator_gradient.py create mode 100644 qiskit/algorithms/gradients/lin_comb_sampler_gradient.py create mode 100644 qiskit/algorithms/gradients/param_shift_estimator_gradient.py create mode 100644 qiskit/algorithms/gradients/param_shift_sampler_gradient.py create mode 100644 qiskit/algorithms/gradients/sampler_gradient_job.py create mode 100644 qiskit/algorithms/gradients/utils.py diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py new file mode 100644 index 000000000000..0edecfba7ff2 --- /dev/null +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -0,0 +1,78 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +""" +Abstract Base class of Gradient for Estimator. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .estimator_gradient_job import EstimatorGradientJob + + +class BaseEstimatorGradient(ABC): + """Base class for an EstimatorGradient to compute the gradients of the expectation value.""" + + def __init__( + self, + estimator: BaseEstimator, + ): + """ + Args: + estimator: The estimator used to compute the gradients. + """ + self._estimator: BaseEstimator = estimator + self._circuits: Sequence[QuantumCircuit] = [] + self._circuit_ids: dict[int, int] = {} + + def run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientJob: + """Run the job of the gradients of expectation values. + + Args: + circuits: The list of quantum circuits to compute the gradients. + observables: The list of observables. + parameter_values: The list of parameter values to be bound to the circuit. + partial: The list of Parameters to calculate only the gradients of the specified parameters. + Defaults to None, which means that the gradients of all parameters will be calculated. + run_options: Backend runtime options used for circuit execution. + + Returns: + The job object of the gradients of the expectation values. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + """ + return self._run(circuits, observables, parameter_values, partial, **run_options) + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientJob: + raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py new file mode 100644 index 000000000000..3307525472f4 --- /dev/null +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -0,0 +1,71 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +""" +Abstract Base class of Gradient for Sampler. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.primitives import BaseSampler +from .sampler_gradient_job import SamplerGradientJob + + +class BaseSamplerGradient(ABC): + """Base class for a SamplerGradient to compute the gradients of the sampling probability.""" + + def __init__(self, sampler: BaseSampler): + """ + Args: + sampler: The sampler used to compute the gradients. + """ + self._sampler: BaseSampler = sampler + self._circuits: list[QuantumCircuit] = [] + self._circuit_ids: dict[int, int] = {} + + def run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientJob: + """Run the job of the gradients of the sampling probability. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + partial: The list of Parameters to calculate only the gradients of the specified parameters. + Defaults to None, which means that the gradients of all parameters will be calculated. + run_options: Backend runtime options used for circuit execution. + + Returns: + The job object of the gradients of the sampling probability. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th + quasi-probability distribution in the i-th result corresponds to the gradients of the + sampling probability for the j-th parameter in ``circuits[i]``. + """ + return self._run(circuits, parameter_values, partial, **run_options) + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientJob: + raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/estimator_gradient_job.py b/qiskit/algorithms/gradients/estimator_gradient_job.py new file mode 100644 index 000000000000..15d94cf95b9f --- /dev/null +++ b/qiskit/algorithms/gradients/estimator_gradient_job.py @@ -0,0 +1,36 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Estimator result class +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from qiskit.primitives import EstimatorResult +from qiskit.providers import JobStatus + + +@dataclass(frozen=True) +class EstimatorGradientJob: + """Result of EstimatorGradient. + + Args: + results (list[EstimatorResult]): List of EstimatorResults. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th value + in the i-th result corresponds to the gradients of the j-th parameter in ``circuits[i]``. + status: List of JobStatus for each EstimatorResult. + """ + + results: list[EstimatorResult] + status: list[JobStatus] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py new file mode 100644 index 000000000000..6e60238fe17a --- /dev/null +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from typing import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .base_estimator_gradient import BaseEstimatorGradient +from .estimator_gradient_job import EstimatorGradientJob +from .utils import make_fin_diff_base_parameter_values + + +class FiniteDiffEstimatorGradient(BaseEstimatorGradient): + """ + Gradient of Estimator with Finite difference method. + """ + + def __init__( + self, + estimator: BaseEstimator, + epsilon: float = 1e-6, + ): + """ + Args: + estimator: The estimator used to compute the gradients. + epsilon: The offset size for the finite difference gradients. + """ + self._epsilon = epsilon + self._base_parameter_values_dict = {} + super().__init__(estimator) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + gradients = [] + status = [] + for circuit, observable, parameter_values_, partial_ in zip( + circuits, observables, parameter_values, partial + ): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_index = index + else: + # if the given circuit is a new one, make base parameter values for + and - epsilon + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._base_parameter_values_dict[ + circuit_index + ] = make_fin_diff_base_parameter_values(circuit, self._epsilon) + self._circuits.append(circuit) + + circuit_parameters = self._circuits[circuit_index].parameters + base_parameter_values_list = [] + gradient_parameter_values = np.zeros(len(circuit_parameters)) + + # a parameter set for the partial option + parameters = partial_ or circuit_parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = {} + # bring the base parameter values for parameters only in the partial parameter set. + for i, param in enumerate(circuit_parameters): + gradient_parameter_values[i] = parameter_values_[i] + if param in param_set: + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][i * 2] + ) + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][i * 2 + 1] + ) + result_index_map[param] = result_index + result_index += 1 + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + gradient_circuits = [self._circuits[circuit_index]] * len( + gradient_parameter_values_list + ) + observable_list = [observable] * len(gradient_parameter_values_list) + + job = self._estimator.run( + gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + ) + results = job.result() + + # Combines the results and coefficients to reconstruct the gradient + # for the original circuit parameters + values = np.zeros(len(parameter_values_)) + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + # plus + values[i] += results.values[result_index_map[param] * 2] / (2 * self._epsilon) + # minus + values[i] -= results.values[result_index_map[param] * 2 + 1] / (2 * self._epsilon) + + gradients.append(EstimatorResult(values, metadata=run_options)) + status.append(job.status()) + return EstimatorGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py new file mode 100644 index 000000000000..a3bc2b5759e5 --- /dev/null +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -0,0 +1,137 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from collections import Counter +from typing import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.result import QuasiDistribution + +from .base_sampler_gradient import BaseSamplerGradient +from .sampler_gradient_job import SamplerGradientJob +from .utils import make_fin_diff_base_parameter_values + + +class FiniteDiffSamplerGradient(BaseSamplerGradient): + """Compute the gradients of the sampling probability with the finite difference method.""" + + def __init__( + self, + sampler: BaseSampler, + epsilon: float = 1e-6, + ): + """ + Args: + sampler: The sampler used to compute the gradients. + epsilon: The offset size for the finite difference gradients. + """ + + self._epsilon = epsilon + self._base_parameter_values_dict = {} + super().__init__(sampler) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + gradients = [] + status = [] + for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_index = index + else: + # if the given circuit a new one, make base parameter values for + and - epsilon + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._base_parameter_values_dict[ + circuit_index + ] = make_fin_diff_base_parameter_values(circuit, self._epsilon) + self._circuits.append(circuit) + + circuit_parameters = self._circuits[circuit_index].parameters + base_parameter_values_list = [] + gradient_parameter_values = np.zeros(len(circuit_parameters)) + + # a parameter set for the partial option + parameters = partial_ or circuit_parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = {} + # bring the base parameter values for parameters only in the partial parameter set. + for i, param in enumerate(circuit_parameters): + gradient_parameter_values[i] = parameter_values_[i] + if param in param_set: + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][i * 2] + ) + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][i * 2 + 1] + ) + result_index_map[param] = result_index + result_index += 1 + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + gradient_circuits = [self._circuits[circuit_index]] * len( + gradient_parameter_values_list + ) + + job = self._sampler.run( + gradient_circuits, gradient_parameter_values_list, **run_options + ) + results = job.result() + + # Combines the results and coefficients to reconstruct the gradient values + # for the original circuit parameters + dists = [Counter() for _ in range(len(parameter_values_))] + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + # plus + dists[i].update( + Counter( + { + k: v / (2 * self._epsilon) + for k, v in results.quasi_dists[result_index_map[param] * 2].items() + } + ) + ) + # minus + dists[i].update( + Counter( + { + k: -1 * v / (2 * self._epsilon) + for k, v in results.quasi_dists[result_index_map[param] * 2 + 1].items() + } + ) + ) + + gradients.append( + SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + ) + status.append(job.status()) + return SamplerGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py new file mode 100644 index 000000000000..17abcbb66917 --- /dev/null +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Gradient of probabilities with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections import defaultdict +from typing import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.quantum_info import Pauli +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .base_estimator_gradient import BaseEstimatorGradient +from .estimator_gradient_job import EstimatorGradientJob +from .utils import make_lin_comb_gradient_circuit + +Pauli_Z = Pauli("Z") + + +class LinCombEstimatorGradient(BaseEstimatorGradient): + """Compute the gradients of the expectation values. + This method employs a linear combination of unitaries, + see e.g. https://arxiv.org/pdf/1811.11184.pdf + """ + + def __init__(self, estimator: BaseEstimator): + """ + Args: + estimator: The estimator used to compute the gradients. + """ + self._gradient_circuit_data_dict = {} + super().__init__(estimator) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + gradients = [] + status = [] + + for circuit, observable, parameter_values_, partial_ in zip( + circuits, observables, parameter_values, partial + ): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_index = index + else: + # if circuit is not passed in the constructor. + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._gradient_circuit_data_dict[circuit_index] = make_lin_comb_gradient_circuit( + circuit + ) + self._circuits.append(circuit) + # Add an observable for the auxiliary qubit + observable_ = observable.expand(Pauli_Z) + + gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] + circuit_parameters = self._circuits[circuit_index].parameters + parameter_value_map = {} + + # a parameter set for the partial option + parameters = partial_ or self._circuits[circuit_index].parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = defaultdict(list) + gradient_circuits = [] + # gradient circuit indices and result indices + for i, param in enumerate(circuit_parameters): + parameter_value_map[param] = parameter_values_[i] + if not param in param_set: + continue + for grad in gradient_circuit_data[param]: + gradient_circuits.append(grad.gradient_circuit) + result_index_map[param].append(result_index) + result_index += 1 + gradient_parameter_values_list = [ + parameter_values_ for i in range(len(gradient_circuits)) + ] + observable_list = [observable_] * len(gradient_circuits) + + job = self._estimator.run( + gradient_circuits, observable_list, gradient_parameter_values_list + ) + results = job.result() + + values = np.zeros(len(circuit_parameters)) + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + for j, grad in enumerate(gradient_circuit_data[param]): + coeff = grad.coeff + result_index = result_index_map[param][j] + # if coeff has parameters, substitute them with the given parameter values + if isinstance(coeff, ParameterExpression): + local_map = {p: parameter_value_map[p] for p in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + values[i] += bound_coeff * results.values[result_index_map[param][j]] + + gradients.append(EstimatorResult(values, metadata=run_options)) + status.append(job.status()) + return EstimatorGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py new file mode 100644 index 000000000000..bb54af2359b2 --- /dev/null +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -0,0 +1,120 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Gradient of probabilities with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections import Counter, defaultdict +from typing import Sequence + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.result import QuasiDistribution + +from .base_sampler_gradient import BaseSamplerGradient +from .sampler_gradient_job import SamplerGradientJob +from .utils import make_lin_comb_gradient_circuit + + +class LinCombSamplerGradient(BaseSamplerGradient): + """Compute the gradients of the sampling probability. + This method employs a linear combination of unitaries, + see e.g. https://arxiv.org/pdf/1811.11184.pdf + """ + + def __init__(self, sampler: BaseSampler): + """ + Args: + sampler: The sampler used to compute the gradients. + """ + + self._gradient_circuit_data_dict = {} + super().__init__(sampler) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + gradients = [] + status = [] + + for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_index = index + else: + # if circuit is not passed in the constructor. + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._gradient_circuit_data_dict[circuit_index] = make_lin_comb_gradient_circuit( + circuit, add_measurement=True + ) + self._circuits.append(circuit) + + gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] + circuit_parameters = self._circuits[circuit_index].parameters + parameter_value_map = {} + + # a parameter set for the partial option + parameters = partial_ or self._circuits[circuit_index].parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = defaultdict(list) + gradient_circuit = [] + # gradient circuit indices and result indices + for i, param in enumerate(circuit_parameters): + parameter_value_map[param] = parameter_values_[i] + if not param in param_set: + continue + for grad in gradient_circuit_data[param]: + gradient_circuit.append(grad.gradient_circuit) + result_index_map[param].append(result_index) + result_index += 1 + gradient_parameter_values_list = [parameter_values_] * len(gradient_circuit) + + job = self._sampler.run( + gradient_circuit, gradient_parameter_values_list, **run_options + ) + results = job.result() + + param_set = set(parameters) + dists = [Counter() for _ in range(len(parameter_values_))] + num_bitstrings = 2 ** self._circuits[circuit_index].num_qubits + i = 0 + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + for j, grad in enumerate(gradient_circuit_data[param]): + coeff = grad.coeff + result_index = result_index_map[param][j] + # if coeff has parameters, substitute them with the given parameter values + if isinstance(coeff, ParameterExpression): + local_map = {p: parameter_value_map[p] for p in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + for k, v in results.quasi_dists[result_index].items(): + sign, k2 = divmod(k, num_bitstrings) + dists[i][k2] += (-1) ** sign * bound_coeff * v + + gradients.append( + SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + ) + status.append(job.status()) + return SamplerGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py new file mode 100644 index 000000000000..c965822704db --- /dev/null +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -0,0 +1,150 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Gradient of probabilities with parameter shift +""" + +from __future__ import annotations + +from typing import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .base_estimator_gradient import BaseEstimatorGradient +from .estimator_gradient_job import EstimatorGradientJob + +from .utils import ( + make_param_shift_gradient_circuit_data, + make_param_shift_base_parameter_values, +) + + +class ParamShiftEstimatorGradient(BaseEstimatorGradient): + """Parameter shift estimator gradient""" + + def __init__( + self, + estimator: BaseEstimator, + ): + """ + Args: + estimator: The estimator used to compute the gradients. + """ + self._gradient_circuit_data_dict = {} + self._base_parameter_values_dict = {} + super().__init__(estimator) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + gradients = [] + status = [] + for circuit, observable, parameter_values_, partial_ in zip( + circuits, observables, parameter_values, partial + ): + circ_index = self._circuit_ids.get(id(circuit)) + if circ_index is not None: + circuit_index = circ_index + else: + # if the given circuit is a new one, make gradient circuit data and base parameter values + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._gradient_circuit_data_dict[ + circuit_index + ] = make_param_shift_gradient_circuit_data(circuit) + self._base_parameter_values_dict[ + circuit_index + ] = make_param_shift_base_parameter_values( + self._gradient_circuit_data_dict[circuit_index] + ) + self._circuits.append(circuit) + + gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] + gradient_parameter_map = gradient_circuit_data.gradient_parameter_map + gradient_parameter_index_map = gradient_circuit_data.gradient_parameter_index_map + circuit_parameters = self._circuits[circuit_index].parameters + parameter_value_map = {} + base_parameter_values_list = [] + gradient_parameter_values = np.zeros( + len(gradient_circuit_data.gradient_circuit.parameters) + ) + + # a parameter set for the partial option + parameters = partial_ or self._circuits[circuit_index].parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = {} + # bring the base parameter values for parameters only in the partial parameter set. + for i, param in enumerate(circuit_parameters): + parameter_value_map[param] = parameter_values_[i] + for g_param in gradient_parameter_map[param]: + g_param_idx = gradient_parameter_index_map[g_param] + gradient_parameter_values[g_param_idx] = parameter_values_[i] + if param in param_set: + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][g_param_idx * 2] + ) + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][g_param_idx * 2 + 1] + ) + result_index_map[g_param] = result_index + result_index += 1 + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + observable_list = [observable] * len(gradient_parameter_values_list) + gradient_circuits = [gradient_circuit_data.gradient_circuit] * len( + gradient_parameter_values_list + ) + + job = self._estimator.run( + gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + ) + results = job.result() + + # Combines the results and coefficients to reconstruct the gradient + # for the original circuit parameters + values = np.zeros(len(parameter_values_)) + + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + for g_param in gradient_parameter_map[param]: + g_param_idx = gradient_parameter_index_map[g_param] + coeff = gradient_circuit_data.coeff_map[g_param] / 2 + # if coeff has parameters, substitute them with the given parameter values + if isinstance(coeff, ParameterExpression): + local_map = {p: parameter_value_map[p] for p in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + # plus + values[i] += bound_coeff * results.values[result_index_map[g_param] * 2] + # minus + values[i] -= bound_coeff * results.values[result_index_map[g_param] * 2 + 1] + gradients.append(EstimatorResult(values, metadata=run_options)) + status.append(job.status()) + return EstimatorGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py new file mode 100644 index 000000000000..dc2593c7d665 --- /dev/null +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -0,0 +1,160 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Gradient of probabilities with parameter shift +""" + +from __future__ import annotations + +from collections import Counter +from typing import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.result import QuasiDistribution + +from .base_sampler_gradient import BaseSamplerGradient +from .sampler_gradient_job import SamplerGradientJob +from .utils import make_param_shift_base_parameter_values, make_param_shift_gradient_circuit_data + + +class ParamShiftSamplerGradient(BaseSamplerGradient): + """Compute the gradients of the sampling probability with the parameter shift method.""" + + def __init__(self, sampler: BaseSampler): + """ + Args: + sampler: The sampler used to compute the gradients. + """ + self._gradient_circuit_data_dict = {} + self._base_parameter_values_dict = {} + super().__init__(sampler) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + partial: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientJob: + partial = partial or [[] for _ in range(len(circuits))] + + gradients = [] + status = [] + for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + index = self._circuit_ids.get(id(circuit)) + if index is not None: + circuit_index = index + else: + # if the given circuit is a new one, make gradient circuit data and base parameter values + circuit_index = len(self._circuits) + self._circuit_ids[id(circuit)] = circuit_index + self._gradient_circuit_data_dict[ + circuit_index + ] = make_param_shift_gradient_circuit_data(circuit) + self._base_parameter_values_dict[ + circuit_index + ] = make_param_shift_base_parameter_values( + self._gradient_circuit_data_dict[circuit_index] + ) + self._circuits.append(circuit) + + gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] + gradient_parameter_map = gradient_circuit_data.gradient_parameter_map + gradient_parameter_index_map = gradient_circuit_data.gradient_parameter_index_map + circuit_parameters = self._circuits[circuit_index].parameters + parameter_value_map = {} + base_parameter_values_list = [] + gradient_parameter_values = np.zeros( + len(gradient_circuit_data.gradient_circuit.parameters) + ) + + # a parameter set for the partial option + parameters = partial_ or self._circuits[circuit_index].parameters + param_set = set(parameters) + + result_index = 0 + result_index_map = {} + # bring the base parameter values for parameters only in the partial parameter set. + for i, param in enumerate(circuit_parameters): + parameter_value_map[param] = parameter_values_[i] + for g_param in gradient_parameter_map[param]: + g_param_idx = gradient_parameter_index_map[g_param] + gradient_parameter_values[g_param_idx] = parameter_values_[i] + if param in param_set: + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][g_param_idx * 2] + ) + base_parameter_values_list.append( + self._base_parameter_values_dict[circuit_index][g_param_idx * 2 + 1] + ) + result_index_map[g_param] = result_index + result_index += 1 + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + gradient_circuits = [gradient_circuit_data.gradient_circuit] * len( + gradient_parameter_values_list + ) + + job = self._sampler.run( + gradient_circuits, gradient_parameter_values_list, **run_options + ) + results = job.result() + + # Combines the results and coefficients to reconstruct the gradient + # for the original circuit parameters + dists = [Counter() for _ in range(len(parameter_values_))] + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + for g_param in gradient_parameter_map[param]: + g_param_idx = gradient_parameter_index_map[g_param] + coeff = gradient_circuit_data.coeff_map[g_param] / 2 + # if coeff has parameters, substitute them with the given parameter values + if isinstance(coeff, ParameterExpression): + local_map = {p: parameter_value_map[p] for p in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + # plus + dists[i].update( + Counter( + { + k: bound_coeff * v + for k, v in results.quasi_dists[ + result_index_map[g_param] * 2 + ].items() + } + ) + ) + # minus + dists[i].update( + Counter( + { + k: -1 * bound_coeff * v + for k, v in results.quasi_dists[ + result_index_map[g_param] * 2 + 1 + ].items() + } + ) + ) + gradients.append( + SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + ) + + status.append(job.status()) + return SamplerGradientJob(results=gradients, status=status) diff --git a/qiskit/algorithms/gradients/sampler_gradient_job.py b/qiskit/algorithms/gradients/sampler_gradient_job.py new file mode 100644 index 000000000000..0a37fd1225b9 --- /dev/null +++ b/qiskit/algorithms/gradients/sampler_gradient_job.py @@ -0,0 +1,37 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Sampler result class +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from qiskit.primitives import SamplerResult +from qiskit.providers import JobStatus + + +@dataclass(frozen=True) +class SamplerGradientJob: + """Result of SamplerGradient. + + Args: + results (list[SamplerResult]): List of SamplerResults. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th + quasi-probability distribution in the i-th result corresponds to the gradients of the + sampling probability for the j-th parameter in ``circuits[i]``. + status: List of JobStatus for each SamplerResult. + """ + + results: list[SamplerResult] + status: list[JobStatus] diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py new file mode 100644 index 000000000000..39c5128e1701 --- /dev/null +++ b/qiskit/algorithms/gradients/utils.py @@ -0,0 +1,343 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. +""" +Utility functions for gradients +""" + +from __future__ import annotations + +from collections import defaultdict +from copy import deepcopy +from dataclasses import dataclass +from typing import Dict, List + +import numpy as np + +from qiskit import transpile +from qiskit.circuit import ( + ClassicalRegister, + Gate, + Instruction, + Parameter, + ParameterExpression, + QuantumCircuit, + QuantumRegister, +) +from qiskit.circuit.library.standard_gates import ( + CXGate, + CYGate, + CZGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZXGate, + RZZGate, +) + + +@dataclass +class ParameterShiftGradientCircuitData: + """Stores gradient circuit data for the parameter shift method + + Args: + circuit (QuantumCircuit): The original quantum circuit + gradient_circuit (QuantumCircuit): An internal quantum circuit used to calculate the gradient + gradient_parameter_map (dict): A dictionary maps the parameters of ``circuit`` to + the parameters of ``gradient_circuit``. + gradient_parameter_index_map (dict): A dictionary maps the parameters of ``gradient_circuit`` + to their index. + gradient_virtual_parameter_map (dict): A dictionary maps the parameters of ``gradient_circuit`` + to the virtual parameter variables. A virtual parameter variable is added if a parameter + expression has more than one parameter. + coeff_map (dict): A dictionary maps the parameters of ``gradient_circuit`` to their coefficients + used to calculate gradients. + """ + + circuit: QuantumCircuit + gradient_circuit: QuantumCircuit + gradient_parameter_map: Dict[Parameter, Parameter] + gradient_parameter_index_map: Dict[Parameter, int] + gradient_virtual_parameter_map: Dict[Parameter, Parameter] + coeff_map: Dict[Parameter, float | ParameterExpression] + + +def make_param_shift_gradient_circuit_data( + circuit: QuantumCircuit, +) -> ParameterShiftGradientCircuitData: + """Makes a gradient circuit data for the parameter shift method. This re-assigns each parameter in + ``circuit`` to a unique parameter, and construct a new gradient circuit with those new + parameters. Also, it makes maps used in later calculations. + + Args: + circuit: The original quantum circuit + + Returns: + necessary data to calculate gradients with the parameter shift method. + """ + + supported_gates = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] + + circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) + g_circuit = circuit2.copy_empty_like(f"g_{circuit2.name}") + param_inst_dict = defaultdict(list) + g_parameter_map = defaultdict(list) + g_virtual_parameter_map = {} + num_virtual_parameter_variables = 0 + coeff_map = {} + + for inst in circuit2.data: + new_inst = deepcopy(inst) + qubit_indices = [circuit2.qubits.index(qubit) for qubit in inst[1]] + new_inst.qubits = tuple(g_circuit.qubits[qubit_index] for qubit_index in qubit_indices) + + # Assign new unique parameters when the instruction is parameterized. + if inst.operation.is_parameterized(): + parameters = inst.operation.params + new_inst_parameters = [] + # For a gate with multiple parameters e.g. a U gate + for parameter in parameters: + subs_map = {} + # For a gate parameter with multiple parameter variables. + # e.g. ry(θ) with θ = (2x + y) + for parameter_variable in parameter.parameters: + if parameter_variable in param_inst_dict: + new_parameter_variable = Parameter( + f"g{parameter_variable.name}_{len(param_inst_dict[parameter_variable])+1}" + ) + else: + new_parameter_variable = Parameter(f"g{parameter_variable.name}_1") + subs_map[parameter_variable] = new_parameter_variable + param_inst_dict[parameter_variable].append(inst) + g_parameter_map[parameter_variable].append(new_parameter_variable) + # Coefficient to calculate derivative i.e. dw/dt in df/dw * dw/dt + coeff_map[new_parameter_variable] = parameter.gradient(parameter_variable) + # Substitute the parameter variables with the corresponding new parameter + # variables in ``subs_map``. + new_parameter = parameter.subs(subs_map) + # If new_parameter is not a single parameter variable, then add a new virtual + # parameter variable. e.g. ry(θ) with θ = (2x + y) becomes ry(θ + virtual_variable) + if not isinstance(new_parameter, Parameter): + virtual_parameter_variable = Parameter( + f"vθ_{num_virtual_parameter_variables+1}" + ) + num_virtual_parameter_variables += 1 + for new_parameter_variable in new_parameter.parameters: + g_virtual_parameter_map[new_parameter_variable] = virtual_parameter_variable + new_parameter = new_parameter + virtual_parameter_variable + new_inst_parameters.append(new_parameter) + new_inst.operation.params = new_inst_parameters + g_circuit.append(new_inst) + + # for global phase + subs_map = {} + if isinstance(g_circuit.global_phase, ParameterExpression): + for parameter_variable in g_circuit.global_phase.parameters: + if parameter_variable in param_inst_dict: + new_parameter_variable = g_parameter_map[parameter_variable][0] + else: + new_parameter_variable = Parameter(f"g{parameter_variable.name}_1") + subs_map[parameter_variable] = new_parameter_variable + g_circuit.global_phase = g_circuit.global_phase.subs(subs_map) + g_parameter_index_map = {} + + for i, g_param in enumerate(g_circuit.parameters): + g_parameter_index_map[g_param] = i + + return ParameterShiftGradientCircuitData( + circuit=circuit2, + gradient_circuit=g_circuit, + gradient_virtual_parameter_map=g_virtual_parameter_map, + gradient_parameter_map=g_parameter_map, + coeff_map=coeff_map, + gradient_parameter_index_map=g_parameter_index_map, + ) + + +def make_param_shift_base_parameter_values( + gradient_circuit_data: ParameterShiftGradientCircuitData, +) -> List[np.ndarray]: + """Makes base parameter values for the parameter shift method. Each base parameter value will + be added to the given parameter values in later calculations. + + Args: + gradient_circuit_data: gradient circuit data for the base parameter values. + + Returns: + The base parameter values for the parameter shift method. + """ + # Make internal parameter values for the parameter shift + num_g_parameters = len(gradient_circuit_data.gradient_circuit.parameters) + base_parameter_values = [] + # Make base decomposed parameter values for each original parameter + for param in gradient_circuit_data.circuit.parameters: + for g_param in gradient_circuit_data.gradient_parameter_map[param]: + # use the related virtual parameter if it exists + if g_param in gradient_circuit_data.gradient_virtual_parameter_map: + g_param = gradient_circuit_data.gradient_virtual_parameter_map[g_param] + g_param_idx = gradient_circuit_data.gradient_parameter_index_map[g_param] + # for + pi/2 in the parameter shift rule + parameter_values_plus = np.zeros(num_g_parameters) + parameter_values_plus[g_param_idx] += np.pi / 2 + base_parameter_values.append(parameter_values_plus) + # for - pi/2 in the parameter shift rule + parameter_values_minus = np.zeros(num_g_parameters) + parameter_values_minus[g_param_idx] -= np.pi / 2 + base_parameter_values.append(parameter_values_minus) + return base_parameter_values + + +def make_fin_diff_base_parameter_values( + circuit: QuantumCircuit, epsilon: float = 1e-6 +) -> List[np.ndarray]: + """Makes base parameter values for the finite difference method. Each base parameter value will + be added to the given parameter values in later calculations. + + Args: + circuit: circuit for the base parameter values. + epsilon: The offset size for the finite difference gradients. + + Returns: + List: The base parameter values for the finite difference method. + """ + base_parameter_values = [] + # Make base decomposed parameter values for each original parameter + for i, _ in enumerate(circuit.parameters): + parameter_values_plus = np.zeros(len(circuit.parameters)) + parameter_values_plus[i] += epsilon + base_parameter_values.append(parameter_values_plus) + # for - epsilon in the finite diff + parameter_values_minus = np.zeros(len(circuit.parameters)) + parameter_values_minus[i] -= epsilon + base_parameter_values.append(parameter_values_minus) + return base_parameter_values + + +@dataclass +class LinearCombGradientCircuit: + """Gradient circuit for the linear combination of unitaries method. + + Args: + gradient_circuit (QuantumCircuit): A gradient circuit for the linear combination + of unitaries method. + coeff (float | ParameterExpression): A coefficient corresponds to the gradient circuit. + """ + + gradient_circuit: QuantumCircuit + coeff: float | ParameterExpression + + +def make_lin_comb_gradient_circuit( + circuit: QuantumCircuit, add_measurement: bool = False +) -> Dict[Parameter, List[LinearCombGradientCircuit]]: + """Makes gradient circuits for the linear combination of unitaries method. + + Args: + circuit: The original quantum circuit. + add_measurement: If True, add measurements to the gradient circuit. Defaults to False. + ``LinCombSamplerGradient`` calls this method with `add_measurement` is True. + + Returns: + A dictionary mapping a parameter to the corresponding list of ``LinearCombGradientCircuit`` + """ + supported_gates = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) + + qr_aux = QuantumRegister(1, "aux") + cr_aux = ClassicalRegister(1, "aux") + circuit2.add_register(qr_aux) + circuit2.add_bits(cr_aux) + circuit2.h(qr_aux) + circuit2.data.insert(0, circuit2.data.pop()) + circuit2.sdg(qr_aux) + circuit2.data.insert(1, circuit2.data.pop()) + + grad_dict = defaultdict(list) + for i, (inst, qregs, _) in enumerate(circuit2.data): + if inst.is_parameterized(): + param = inst.params[0] + for p in param.parameters: + gate = _gate_gradient(inst) + circuit3 = circuit2.copy() + # insert `gate` to i-th position + circuit3.append(gate, [qr_aux[0]] + qregs, []) + circuit3.data.insert(i, circuit3.data.pop()) + circuit3.h(qr_aux) + if add_measurement: + circuit3.measure(qr_aux, cr_aux) + grad_dict[p].append(LinearCombGradientCircuit(circuit3, param.gradient(p))) + + return grad_dict + + +def _gate_gradient(gate: Gate) -> Instruction: + """Returns the derivative of the gate""" + if isinstance(gate, RXGate): + # theta + return CXGate() + if isinstance(gate, RYGate): + # theta + return CYGate() + if isinstance(gate, RZGate): + # theta + return CZGate() + if isinstance(gate, RXXGate): + # theta + cxx_circ = QuantumCircuit(3) + cxx_circ.cx(0, 1) + cxx_circ.cx(0, 2) + cxx = cxx_circ.to_instruction() + return cxx + if isinstance(gate, RYYGate): + # theta + cyy_circ = QuantumCircuit(3) + cyy_circ.cy(0, 1) + cyy_circ.cy(0, 2) + cyy = cyy_circ.to_instruction() + return cyy + if isinstance(gate, RZZGate): + # theta + czz_circ = QuantumCircuit(3) + czz_circ.cz(0, 1) + czz_circ.cz(0, 2) + czz = czz_circ.to_instruction() + return czz + if isinstance(gate, RZXGate): + # theta + czx_circ = QuantumCircuit(3) + czx_circ.cx(0, 2) + czx_circ.cz(0, 1) + czx = czx_circ.to_instruction() + return czx + raise TypeError(f"Unrecognized parameterized gate, {gate}") From ad81449cc8608e7ea6de15f05db0d20b00e4131d Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Tue, 16 Aug 2022 20:19:01 +0900 Subject: [PATCH 02/37] add run_options and supported gate Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_estimator_gradient.py | 5 +++++ qiskit/algorithms/gradients/base_sampler_gradient.py | 6 +++++- .../algorithms/gradients/finite_diff_estimator_gradient.py | 3 ++- qiskit/algorithms/gradients/finite_diff_sampler_gradient.py | 3 ++- qiskit/algorithms/gradients/lin_comb_estimator_gradient.py | 4 ++-- qiskit/algorithms/gradients/lin_comb_sampler_gradient.py | 4 ++-- .../algorithms/gradients/param_shift_estimator_gradient.py | 3 ++- qiskit/algorithms/gradients/param_shift_sampler_gradient.py | 4 ++-- qiskit/algorithms/gradients/utils.py | 2 +- 9 files changed, 23 insertions(+), 11 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index 0edecfba7ff2..dbb0674a5d3c 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -33,6 +33,7 @@ class BaseEstimatorGradient(ABC): def __init__( self, estimator: BaseEstimator, + **run_options, ): """ Args: @@ -41,6 +42,7 @@ def __init__( self._estimator: BaseEstimator = estimator self._circuits: Sequence[QuantumCircuit] = [] self._circuit_ids: dict[int, int] = {} + self._default_run_options = run_options def run( self, @@ -64,6 +66,9 @@ def run( The job object of the gradients of the expectation values. The i-th result corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. """ + # The priority of run option is as follows: + # run_options in `run` method > gradient's default run_options > primitive's default setting. + run_options = run_options or self._default_run_options return self._run(circuits, observables, parameter_values, partial, **run_options) @abstractmethod diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 3307525472f4..23a455a7c168 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -27,7 +27,7 @@ class BaseSamplerGradient(ABC): """Base class for a SamplerGradient to compute the gradients of the sampling probability.""" - def __init__(self, sampler: BaseSampler): + def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. @@ -35,6 +35,7 @@ def __init__(self, sampler: BaseSampler): self._sampler: BaseSampler = sampler self._circuits: list[QuantumCircuit] = [] self._circuit_ids: dict[int, int] = {} + self._default_run_options = run_options def run( self, @@ -58,6 +59,9 @@ def run( quasi-probability distribution in the i-th result corresponds to the gradients of the sampling probability for the j-th parameter in ``circuits[i]``. """ + # The priority of run option is as follows: + # run_options in `run` method > gradient's default run_options > primitive's default run_options. + run_options = run_options or self._default_run_options return self._run(circuits, parameter_values, partial, **run_options) @abstractmethod diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 6e60238fe17a..00814b70a0ac 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -37,6 +37,7 @@ def __init__( self, estimator: BaseEstimator, epsilon: float = 1e-6, + **run_options ): """ Args: @@ -45,7 +46,7 @@ def __init__( """ self._epsilon = epsilon self._base_parameter_values_dict = {} - super().__init__(estimator) + super().__init__(estimator, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index a3bc2b5759e5..4cbb09f068ea 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -35,6 +35,7 @@ def __init__( self, sampler: BaseSampler, epsilon: float = 1e-6, + **run_options, ): """ Args: @@ -44,7 +45,7 @@ def __init__( self._epsilon = epsilon self._base_parameter_values_dict = {} - super().__init__(sampler) + super().__init__(sampler, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 17abcbb66917..df9e81bc2835 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -39,13 +39,13 @@ class LinCombEstimatorGradient(BaseEstimatorGradient): see e.g. https://arxiv.org/pdf/1811.11184.pdf """ - def __init__(self, estimator: BaseEstimator): + def __init__(self, estimator: BaseEstimator, **run_options): """ Args: estimator: The estimator used to compute the gradients. """ self._gradient_circuit_data_dict = {} - super().__init__(estimator) + super().__init__(estimator, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index bb54af2359b2..65bfd3f69095 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -33,14 +33,14 @@ class LinCombSamplerGradient(BaseSamplerGradient): see e.g. https://arxiv.org/pdf/1811.11184.pdf """ - def __init__(self, sampler: BaseSampler): + def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. """ self._gradient_circuit_data_dict = {} - super().__init__(sampler) + super().__init__(sampler, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index c965822704db..7312fef43bfd 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -39,6 +39,7 @@ class ParamShiftEstimatorGradient(BaseEstimatorGradient): def __init__( self, estimator: BaseEstimator, + **run_options ): """ Args: @@ -46,7 +47,7 @@ def __init__( """ self._gradient_circuit_data_dict = {} self._base_parameter_values_dict = {} - super().__init__(estimator) + super().__init__(estimator, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index dc2593c7d665..80a077065a11 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -32,14 +32,14 @@ class ParamShiftSamplerGradient(BaseSamplerGradient): """Compute the gradients of the sampling probability with the parameter shift method.""" - def __init__(self, sampler: BaseSampler): + def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. """ self._gradient_circuit_data_dict = {} self._base_parameter_values_dict = {} - super().__init__(sampler) + super().__init__(sampler, **run_options) def _run( self, diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 39c5128e1701..521f55b0d4b1 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -86,7 +86,7 @@ def make_param_shift_gradient_circuit_data( necessary data to calculate gradients with the parameter shift method. """ - supported_gates = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] + supported_gates = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz", "ryy", "rxx", "rzz"] circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) g_circuit = circuit2.copy_empty_like(f"g_{circuit2.name}") From 7ee8f283a533df7884b81d4833fab88030d7bb59 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Mon, 22 Aug 2022 21:52:39 +0900 Subject: [PATCH 03/37] added unittests Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/__init__.py | 74 +++ .../gradients/base_estimator_gradient.py | 51 ++- .../gradients/base_sampler_gradient.py | 36 +- ...nt_job.py => estimator_gradient_result.py} | 8 +- .../finite_diff_estimator_gradient.py | 10 +- .../gradients/finite_diff_sampler_gradient.py | 10 +- .../gradients/lin_comb_estimator_gradient.py | 10 +- .../gradients/lin_comb_sampler_gradient.py | 10 +- .../param_shift_estimator_gradient.py | 10 +- .../gradients/param_shift_sampler_gradient.py | 10 +- ...ient_job.py => sampler_gradient_result.py} | 9 +- .../algorithms/test_estimator_gradient.py | 289 ++++++++++++ .../algorithms/test_sampler_gradient.py | 423 ++++++++++++++++++ 13 files changed, 901 insertions(+), 49 deletions(-) create mode 100644 qiskit/algorithms/gradients/__init__.py rename qiskit/algorithms/gradients/{estimator_gradient_job.py => estimator_gradient_result.py} (88%) rename qiskit/algorithms/gradients/{sampler_gradient_job.py => sampler_gradient_result.py} (86%) create mode 100644 test/python/algorithms/test_estimator_gradient.py create mode 100644 test/python/algorithms/test_sampler_gradient.py diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py new file mode 100644 index 000000000000..487a5bd96e4b --- /dev/null +++ b/qiskit/algorithms/gradients/__init__.py @@ -0,0 +1,74 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +""" +===================================== +Gradients (:mod:`qiskit.algorithms.gradients`) +===================================== + +.. currentmodule:: qiskit.algorithms.gradients + +Base Classes +============ + +.. autosummary:: + :toctree: ../stubs/ + + BaseSamplerGradient + BaseEstimatorGradient + +Gradients +========= + +.. autosummary:: + :toctree: ../stubs/ + + FiniteDiffEstimatorGradient + FiniteDiffSamplerGradient + LinCombEstimatorGradient + LinCombSamplerGradient + ParamShiftEstimatorGradient + ParamShiftSamplerGradient + +Results +======= + +.. autosummary:: + :toctree: ../stubs/ + + EstimatorResult + SamplerResult +""" + +from .base_estimator_gradient import BaseEstimatorGradient +from .base_sampler_gradient import BaseSamplerGradient +from .estimator_gradient_result import EstimatorGradientResult +from .finite_diff_estimator_gradient import FiniteDiffEstimatorGradient +from .finite_diff_sampler_gradient import FiniteDiffSamplerGradient +from .lin_comb_estimator_gradient import LinCombEstimatorGradient +from .lin_comb_sampler_gradient import LinCombSamplerGradient +from .param_shift_estimator_gradient import ParamShiftEstimatorGradient +from .param_shift_sampler_gradient import ParamShiftSamplerGradient +from .sampler_gradient_result import SamplerGradientResult + +__all__ = [ + "BaseEstimatorGradient", + "BaseSamplerGradient", + "EstimatorGradientResult", + "FiniteDiffEstimatorGradient", + "FiniteDiffSamplerGradient", + "LinCombEstimatorGradient", + "LinCombSamplerGradient", + "ParamShiftEstimatorGradient", + "ParamShiftSamplerGradient", + "SamplerGradientResult", +] diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index dbb0674a5d3c..64c0e621fd77 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -20,11 +20,12 @@ from collections.abc import Sequence from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator -from .estimator_gradient_job import EstimatorGradientJob +from .estimator_gradient_result import EstimatorGradientResult class BaseEstimatorGradient(ABC): @@ -44,14 +45,14 @@ def __init__( self._circuit_ids: dict[int, int] = {} self._default_run_options = run_options - def run( + def evaluate( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> EstimatorGradientJob: + ) -> EstimatorGradientResult: """Run the job of the gradients of expectation values. Args: @@ -65,19 +66,57 @@ def run( Returns: The job object of the gradients of the expectation values. The i-th result corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + + Raises: + QiskitError: Invalid arguments are given. """ + # Validation + if len(circuits) != len(parameter_values): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + if len(circuits) != len(observables): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of observables ({len(partial)})." + ) + + if partial is not None: + if len(circuits) != len(partial): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of partial parameter sets ({len(partial)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if len(parameter_value) != circuit.num_parameters: + raise QiskitError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + for i, (circuit, observable) in enumerate(zip(circuits, observables)): + if circuit.num_qubits != observable.num_qubits: + raise QiskitError( + f"The number of qubits of the {i}-th circuit ({circuit.num_qubits}) does " + f"not match the number of qubits of the {i}-th observable " + f"({observable.num_qubits})." + ) + # The priority of run option is as follows: # run_options in `run` method > gradient's default run_options > primitive's default setting. run_options = run_options or self._default_run_options - return self._run(circuits, observables, parameter_values, partial, **run_options) + return self._evaluate(circuits, observables, parameter_values, partial, **run_options) @abstractmethod - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> EstimatorGradientJob: + ) -> EstimatorGradientResult: raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 23a455a7c168..4532f0ac96c0 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -20,8 +20,9 @@ from collections.abc import Sequence from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler -from .sampler_gradient_job import SamplerGradientJob +from .sampler_gradient_result import SamplerGradientResult class BaseSamplerGradient(ABC): @@ -37,13 +38,13 @@ def __init__(self, sampler: BaseSampler, **run_options): self._circuit_ids: dict[int, int] = {} self._default_run_options = run_options - def run( + def evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> SamplerGradientJob: + ) -> SamplerGradientResult: """Run the job of the gradients of the sampling probability. Args: @@ -58,18 +59,41 @@ def run( ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th quasi-probability distribution in the i-th result corresponds to the gradients of the sampling probability for the j-th parameter in ``circuits[i]``. + + Raises: + QiskitError: Invalid arguments are given. """ + # Validation + if len(circuits) != len(parameter_values): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + if partial is not None: + if len(circuits) != len(partial): + raise QiskitError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of partial parameter sets ({len(partial)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if len(parameter_value) != circuit.num_parameters: + raise QiskitError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + # The priority of run option is as follows: # run_options in `run` method > gradient's default run_options > primitive's default run_options. run_options = run_options or self._default_run_options - return self._run(circuits, parameter_values, partial, **run_options) + return self._evaluate(circuits, parameter_values, partial, **run_options) @abstractmethod - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> SamplerGradientJob: + ) -> SamplerGradientResult: raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/estimator_gradient_job.py b/qiskit/algorithms/gradients/estimator_gradient_result.py similarity index 88% rename from qiskit/algorithms/gradients/estimator_gradient_job.py rename to qiskit/algorithms/gradients/estimator_gradient_result.py index 15d94cf95b9f..f4c5c1b11014 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_job.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -15,14 +15,15 @@ from __future__ import annotations +import numpy as np +from typing import Any from dataclasses import dataclass -from qiskit.primitives import EstimatorResult from qiskit.providers import JobStatus @dataclass(frozen=True) -class EstimatorGradientJob: +class EstimatorGradientResult: """Result of EstimatorGradient. Args: @@ -32,5 +33,6 @@ class EstimatorGradientJob: status: List of JobStatus for each EstimatorResult. """ - results: list[EstimatorResult] + values: list[np.ndarray] status: list[JobStatus] + metadata: list[dict[str, Any]] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 00814b70a0ac..d9040e3cacb8 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -24,7 +24,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_job import EstimatorGradientJob +from .estimator_gradient_result import EstimatorGradientResult from .utils import make_fin_diff_base_parameter_values @@ -48,14 +48,14 @@ def __init__( self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> EstimatorGradientJob: + ) -> EstimatorGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] status = [] @@ -122,6 +122,6 @@ def _run( # minus values[i] -= results.values[result_index_map[param] * 2 + 1] / (2 * self._epsilon) - gradients.append(EstimatorResult(values, metadata=run_options)) + gradients.append(values) status.append(job.status()) - return EstimatorGradientJob(results=gradients, status=status) + return EstimatorGradientResult(values=gradients, status=status,metadata=run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 4cbb09f068ea..561b195aa53f 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -24,7 +24,7 @@ from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_job import SamplerGradientJob +from .sampler_gradient_result import SamplerGradientResult from .utils import make_fin_diff_base_parameter_values @@ -47,13 +47,13 @@ def __init__( self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> SamplerGradientJob: + ) -> SamplerGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] status = [] @@ -132,7 +132,7 @@ def _run( ) gradients.append( - SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + [QuasiDistribution(dist) for dist in dists] ) status.append(job.status()) - return SamplerGradientJob(results=gradients, status=status) + return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index df9e81bc2835..e16182a30c2d 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -27,7 +27,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_job import EstimatorGradientJob +from .estimator_gradient_result import EstimatorGradientResult from .utils import make_lin_comb_gradient_circuit Pauli_Z = Pauli("Z") @@ -47,14 +47,14 @@ def __init__(self, estimator: BaseEstimator, **run_options): self._gradient_circuit_data_dict = {} super().__init__(estimator, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> EstimatorGradientJob: + ) -> EstimatorGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] status = [] @@ -121,6 +121,6 @@ def _run( bound_coeff = coeff values[i] += bound_coeff * results.values[result_index_map[param][j]] - gradients.append(EstimatorResult(values, metadata=run_options)) + gradients.append(values) status.append(job.status()) - return EstimatorGradientJob(results=gradients, status=status) + return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 65bfd3f69095..de9500286f0c 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -23,7 +23,7 @@ from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_job import SamplerGradientJob +from .sampler_gradient_result import SamplerGradientResult from .utils import make_lin_comb_gradient_circuit @@ -42,13 +42,13 @@ def __init__(self, sampler: BaseSampler, **run_options): self._gradient_circuit_data_dict = {} super().__init__(sampler, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> SamplerGradientJob: + ) -> SamplerGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] status = [] @@ -114,7 +114,7 @@ def _run( dists[i][k2] += (-1) ** sign * bound_coeff * v gradients.append( - SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + [QuasiDistribution(dist) for dist in dists] ) status.append(job.status()) - return SamplerGradientJob(results=gradients, status=status) + return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 7312fef43bfd..2136d9b76482 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -25,7 +25,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_job import EstimatorGradientJob +from .estimator_gradient_result import EstimatorGradientResult from .utils import ( make_param_shift_gradient_circuit_data, @@ -49,14 +49,14 @@ def __init__( self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> EstimatorGradientJob: + ) -> EstimatorGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] status = [] @@ -146,6 +146,6 @@ def _run( values[i] += bound_coeff * results.values[result_index_map[g_param] * 2] # minus values[i] -= bound_coeff * results.values[result_index_map[g_param] * 2 + 1] - gradients.append(EstimatorResult(values, metadata=run_options)) + gradients.append(values) status.append(job.status()) - return EstimatorGradientJob(results=gradients, status=status) + return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 80a077065a11..dd954878bec5 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -25,7 +25,7 @@ from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_job import SamplerGradientJob +from .sampler_gradient_result import SamplerGradientResult from .utils import make_param_shift_base_parameter_values, make_param_shift_gradient_circuit_data @@ -41,13 +41,13 @@ def __init__(self, sampler: BaseSampler, **run_options): self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) - def _run( + def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], partial: Sequence[Sequence[Parameter]] | None = None, **run_options, - ) -> SamplerGradientJob: + ) -> SamplerGradientResult: partial = partial or [[] for _ in range(len(circuits))] gradients = [] @@ -153,8 +153,8 @@ def _run( ) ) gradients.append( - SamplerResult([QuasiDistribution(dist) for dist in dists], metadata=run_options) + [QuasiDistribution(dist) for dist in dists] ) status.append(job.status()) - return SamplerGradientJob(results=gradients, status=status) + return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/sampler_gradient_job.py b/qiskit/algorithms/gradients/sampler_gradient_result.py similarity index 86% rename from qiskit/algorithms/gradients/sampler_gradient_job.py rename to qiskit/algorithms/gradients/sampler_gradient_result.py index 0a37fd1225b9..5949194915a1 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_job.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -15,14 +15,15 @@ from __future__ import annotations +from typing import Any from dataclasses import dataclass -from qiskit.primitives import SamplerResult +from qiskit.result import QuasiDistribution from qiskit.providers import JobStatus @dataclass(frozen=True) -class SamplerGradientJob: +class SamplerGradientResult: """Result of SamplerGradient. Args: @@ -32,6 +33,6 @@ class SamplerGradientJob: sampling probability for the j-th parameter in ``circuits[i]``. status: List of JobStatus for each SamplerResult. """ - - results: list[SamplerResult] + quasi_dists: list[list[QuasiDistribution]] status: list[JobStatus] + metadata: list[dict[str, Any]] diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py new file mode 100644 index 000000000000..d3184180bae8 --- /dev/null +++ b/test/python/algorithms/test_estimator_gradient.py @@ -0,0 +1,289 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2021. +# +# 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. +# ============================================================================= + +""" Test Quantum Gradient Framework """ + +import unittest +from test import combine + +import numpy as np +from ddt import ddt + +from qiskit import QuantumCircuit +from qiskit.algorithms.gradients.finite_diff_estimator_gradient import FiniteDiffEstimatorGradient +from qiskit.algorithms.gradients.lin_comb_estimator_gradient import LinCombEstimatorGradient +from qiskit.algorithms.gradients.param_shift_estimator_gradient import ParamShiftEstimatorGradient +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.exceptions import QiskitError +from qiskit.primitives import Estimator +from qiskit.quantum_info import SparsePauliOp +from qiskit.test import QiskitTestCase + + +@ddt +class TestEstimatorGradient(QiskitTestCase): + """Test Estimator Gradient""" + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_p(self, grad): + """Test the estimator gradient for p""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + param_list = [[np.pi / 4], [0], [np.pi / 2]] + correct_results = [[-1 / np.sqrt(2)], [0], [-1]] + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + for j, value in enumerate(values): + self.assertAlmostEqual(value, correct_results[i][j], 3) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_u(self, grad): + """Test the estimator gradient for u""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + qc = QuantumCircuit(1) + qc.h(0) + qc.u(a, b, c, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + + param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] + correct_results = [[-0.70710678, 0.0, 0.0], [-0.35355339, -0.85355339, -0.85355339]] + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + for j, value in enumerate(values): + self.assertAlmostEqual(value, correct_results[i][j], 3) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_efficient_su2(self, grad): + """Test the estimator gradient for EfficientSU2""" + estimator = Estimator() + qc = EfficientSU2(2, reps=1) + op = SparsePauliOp.from_list([("ZI", 1)]) + gradient = grad(estimator) + param_list = [ + [np.pi / 4 for param in qc.parameters], + [np.pi / 2 for param in qc.parameters], + ] + correct_results = [ + [ + -0.35355339, + -0.70710678, + 0, + 0.35355339, + 0, + -0.70710678, + 0, + 0, + ], + [0, 0, 0, 1, 0, 0, 0, 0], + ] + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_rxx(self, grad): + """Test the estimator gradient for rxx""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.rxx(a, 0, 1) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_ryy(self, grad): + """Test the estimator gradient for ryy""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.ryy(a, 0, 1) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_rzz(self, grad): + """Test the estimator gradient for rzz""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.rzz(a, 0, 1) + qc.h([0, 1]) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_rzx(self, grad): + """Test the estimator gradient for rzx""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.rzx(a, 0, 1) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_parameter_coefficient(self, grad): + """Test the estimator gradient for parameter variables with coefficients""" + estimator = Estimator() + qc = RealAmplitudes(num_qubits=2, reps=1) + qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) + qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) + qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) + qc.p(2 * qc.parameters[0] + 1, 0) + qc.rxx(qc.parameters[0] + 2, 0, 1) + gradient = grad(estimator) + param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] + correct_results = [ + [-0.7266653, -0.4905135, -0.0068606, -0.9228880], + [-3.5972095, 0.10237173, -0.3117748, 0], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + def test_gradient_partial(self, grad): + """Test the estimator gradient for partial""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4, np.pi / 2]] + correct_results = [ + [-0.70710678, 0], + ] + op = SparsePauliOp.from_list([("Z", 1)]) + for i, param in enumerate(param_list): + values = gradient.evaluate([qc], [op], [param]).values[0] + np.testing.assert_almost_equal(values, correct_results[i]) + + @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + def test_gradient_multi_arguments(self, grad): + """Test the estimator gradient for multiple arguments""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc2 = QuantumCircuit(1) + qc2.rx(b, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("Z", 1)]) + values = gradient.evaluate([qc,qc2], [op]*2, param_list).values + np.testing.assert_almost_equal(values, correct_results) + + c = Parameter("c") + qc3 = QuantumCircuit(1) + qc3.rx(c, 0) + qc3.rz(a, 0) + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] + correct_results2 = [ + [-0.70710678], + [-0.70710678 if p==c else 0 for p in qc3.parameters], + ] + values2 = gradient.evaluate([qc,qc3], [op]*2, param_list2, partial=[[a],[c]]).values + np.testing.assert_almost_equal(values2[0], correct_results2[0]) + np.testing.assert_almost_equal(values2[1], correct_results2[1]) + + @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + def test_gradient_validation(self, grad): + """Test estimator gradient's validation""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + op = SparsePauliOp.from_list([("Z", 1)]) + with self.assertRaises(QiskitError): + gradient.evaluate([qc], [op], param_list) + with self.assertRaises(QiskitError): + gradient.evaluate([qc, qc], [op, op], param_list, partial=[[a]]) + with self.assertRaises(QiskitError): + gradient.evaluate([qc, qc], [op], param_list, partial=[[a]]) + with self.assertRaises(QiskitError): + gradient.evaluate([qc], [op], [[np.pi / 4,np.pi / 4]]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py new file mode 100644 index 000000000000..1878f2afd5ff --- /dev/null +++ b/test/python/algorithms/test_sampler_gradient.py @@ -0,0 +1,423 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019, 2021. +# +# 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. +# ============================================================================= + +""" Test Quantum Gradient Framework """ + +import unittest +from test import combine + +import numpy as np +from ddt import ddt + +from qiskit import QuantumCircuit +from qiskit.algorithms.gradients.finite_diff_sampler_gradient import \ + FiniteDiffSamplerGradient +from qiskit.algorithms.gradients.lin_comb_sampler_gradient import \ + LinCombSamplerGradient +from qiskit.algorithms.gradients.param_shift_sampler_gradient import \ + ParamShiftSamplerGradient +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.exceptions import QiskitError +from qiskit.primitives import Sampler +from qiskit.test import QiskitTestCase + + +@ddt +class TestSamplerGradient(QiskitTestCase): + """Test Sampler Gradient""" + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_p(self, grad): + """Test the sampler gradient for p""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [0], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: 0, 1: 0}], + [{0: -0.499999, 1: 0.499999}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_u(self, grad): + """Test the sampler gradient for u""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + qc = QuantumCircuit(1) + qc.h(0) + qc.u(a, b, c, 0) + qc.h(0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {0: 0, 1: 0}, {0: 0, 1: 0}], + [{0: -0.176777, 1: 0.176777}, {0: -0.426777, 1: 0.426777}, {0: -0.426777, 1: 0.426777}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_efficient_su2(self, grad): + """Test the sampler gradient for EfficientSU2""" + sampler = Sampler() + qc = EfficientSU2(2, reps=1) + qc.measure_all() + gradient = grad(sampler) + param_list = [ + [np.pi / 4 for param in qc.parameters], + [np.pi / 2 for param in qc.parameters], + ] + correct_results = [ + [ + { + 0: -0.11963834764831836, + 1: -0.05713834764831845, + 2: -0.21875000000000003, + 3: 0.39552669529663675, + }, + { + 0: -0.32230339059327373, + 1: -0.031250000000000014, + 2: 0.2339150429449554, + 3: 0.11963834764831843, + }, + { + 0: 0.012944173824159189, + 1: -0.01294417382415923, + 2: 0.07544417382415919, + 3: -0.07544417382415919, + }, + { + 0: 0.2080266952966367, + 1: -0.03125000000000002, + 2: -0.11963834764831842, + 3: -0.057138347648318405, + }, + { + 0: -0.11963834764831838, + 1: 0.11963834764831838, + 2: -0.21875000000000003, + 3: 0.21875, + }, + { + 0: -0.2781092167691146, + 1: -0.0754441738241592, + 2: 0.27810921676911443, + 3: 0.07544417382415924, + }, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + ], + [ + { + 0: -4.163336342344337e-17, + 1: 2.7755575615628914e-17, + 2: -4.163336342344337e-17, + 3: 0.0, + }, + {0: 0.0, 1: -1.3877787807814457e-17, 2: 4.163336342344337e-17, 3: 0.0}, + { + 0: -0.24999999999999994, + 1: 0.24999999999999994, + 2: 0.24999999999999994, + 3: -0.24999999999999994, + }, + { + 0: 0.24999999999999994, + 1: 0.24999999999999994, + 2: -0.24999999999999994, + 3: -0.24999999999999994, + }, + { + 0: -4.163336342344337e-17, + 1: 4.163336342344337e-17, + 2: -4.163336342344337e-17, + 3: 5.551115123125783e-17, + }, + { + 0: -0.24999999999999994, + 1: 0.24999999999999994, + 2: 0.24999999999999994, + 3: -0.24999999999999994, + }, + {0: 0.0, 1: 2.7755575615628914e-17, 2: 0.0, 3: 2.7755575615628914e-17}, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + ], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_rxx(self, grad): + """Test the sampler gradient for rxx""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.rxx(a, 0, 1) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], + [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_ryy(self, grad): + """Test the sampler gradient for ryy""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.ryy(a, 0, 1) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], + [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_rzz(self, grad): + """Test the sampler gradient for rzz""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.h([0, 1]) + qc.rzz(a, 0, 1) + qc.h([0, 1]) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], + [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_rzx(self, grad): + """Test the sampler gradient for rzx""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(2) + qc.rzx(a, 0, 1) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0.5 / np.sqrt(2), 3: 0}], + [{0: -0.5, 1: 0, 2: 0.5, 3: 0}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_parameter_coefficient(self, grad): + """Test the sampler gradient for parameter variables with coefficients""" + sampler = Sampler() + qc = RealAmplitudes(num_qubits=2, reps=1) + qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) + qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) + qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) + qc.p(2 * qc.parameters[0] + 1, 0) + qc.rxx(qc.parameters[0] + 2, 0, 1) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] + correct_results = [ + [ + { + 0: 0.30014831912265927, + 1: -0.6634809704357856, + 2: 0.343589357193753, + 3: 0.019743294119373426, + }, + { + 0: 0.16470607453981906, + 1: -0.40996282450610577, + 2: 0.08791803062881773, + 3: 0.15733871933746948, + }, + { + 0: 0.27036068339663866, + 1: -0.273790986018701, + 2: 0.12752010079553433, + 3: -0.12408979817347202, + }, + { + 0: -0.2098616294167757, + 1: -0.2515823946449894, + 2: 0.21929102305386305, + 3: 0.24215300100790207, + }, + ], + [ + { + 0: -1.844810060881004, + 1: 0.04620532700836027, + 2: 1.6367366426074323, + 3: 0.16186809126521057, + }, + { + 0: 0.07296073407769421, + 1: -0.021774869186331716, + 2: 0.02177486918633173, + 3: -0.07296073407769456, + }, + { + 0: -0.07794369186049102, + 1: -0.07794369186049122, + 2: 0.07794369186049117, + 3: 0.07794369186049112, + }, + { + 0: 0.0, + 1: 0.0, + 2: 0.0, + 3: 0.0, + }, + ], + ] + + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_partial(self, grad): + """Test the sampler gradient for partial""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4, np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], + ] + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param],partial=[[a]]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + if correct_results[i][j]: + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + else: + self.assertEqual(quasi_dist, {}) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_multi_arguments(self, grad): + """Test the sampler gradient for multiple arguments""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.rx(b, 0) + qc2.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: -0.499999, 1: 0.499999}], + ] + quasi_dists = gradient.evaluate([qc,qc2], param_list).quasi_dists + for j, q_dists in enumerate(quasi_dists): + quasi_dist = q_dists[0] + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[j][0][k], 3) + + c = Parameter("c") + qc3 = QuantumCircuit(1) + qc3.rx(c, 0) + qc3.rz(a, 0) + qc3.measure_all() + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] + quasi_dists = gradient.evaluate([qc,qc3], param_list2, partial=[[a],[c]]).quasi_dists + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)} if p==c else {} for p in qc3.parameters] + ] + for i, result in enumerate(quasi_dists): + for j, q_dists in enumerate(result): + if correct_results[i][j]: + for k in q_dists: + self.assertAlmostEqual(q_dists[k], correct_results[i][j][k], 3) + else: + self.assertEqual(q_dists, {}) + + @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_validation(self, grad): + """Test sampler gradient's validation""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + with self.assertRaises(QiskitError): + gradient.evaluate([qc], param_list) + with self.assertRaises(QiskitError): + gradient.evaluate([qc, qc], param_list, partial=[[a]]) + with self.assertRaises(QiskitError): + gradient.evaluate([qc], [[np.pi / 4,np.pi / 4]]) + + +if __name__ == "__main__": + unittest.main() From b7ad4aa7bd0c7b1663239539ee92f52cd9fdf3b9 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Tue, 23 Aug 2022 00:12:18 +0900 Subject: [PATCH 04/37] lint Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/estimator_gradient_result.py | 5 +++-- .../finite_diff_estimator_gradient.py | 11 +++------- .../gradients/finite_diff_sampler_gradient.py | 6 ++---- .../gradients/lin_comb_estimator_gradient.py | 2 +- .../gradients/lin_comb_sampler_gradient.py | 10 +++------ .../param_shift_estimator_gradient.py | 11 ++++------ .../gradients/param_shift_sampler_gradient.py | 11 +++++----- .../gradients/sampler_gradient_result.py | 1 + qiskit/algorithms/gradients/utils.py | 21 ++++++++++++++++++- ...gradients-primitives-561cf9cf75a7ccb8.yaml | 14 +++++++++++++ .../algorithms/test_estimator_gradient.py | 20 +++++++++++------- .../algorithms/test_sampler_gradient.py | 19 +++++++---------- 12 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index f4c5c1b11014..e813f19c2d26 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -15,9 +15,10 @@ from __future__ import annotations -import numpy as np -from typing import Any from dataclasses import dataclass +from typing import Any + +import numpy as np from qiskit.providers import JobStatus diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index d9040e3cacb8..340fba1bf2a3 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -20,7 +20,7 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -33,12 +33,7 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient): Gradient of Estimator with Finite difference method. """ - def __init__( - self, - estimator: BaseEstimator, - epsilon: float = 1e-6, - **run_options - ): + def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_options): """ Args: estimator: The estimator used to compute the gradients. @@ -124,4 +119,4 @@ def _evaluate( gradients.append(values) status.append(job.status()) - return EstimatorGradientResult(values=gradients, status=status,metadata=run_options) + return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 561b195aa53f..f299bb3ee80d 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -20,7 +20,7 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.primitives import BaseSampler from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient @@ -131,8 +131,6 @@ def _evaluate( ) ) - gradients.append( - [QuasiDistribution(dist) for dist in dists] - ) + gradients.append([QuasiDistribution(dist) for dist in dists]) status.append(job.status()) return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index e16182a30c2d..b7ac3571b199 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -22,7 +22,7 @@ from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.primitives import BaseEstimator from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.base_operator import BaseOperator diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index de9500286f0c..3b95252b99f8 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -19,7 +19,7 @@ from typing import Sequence from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.primitives import BaseSampler from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient @@ -88,9 +88,7 @@ def _evaluate( result_index += 1 gradient_parameter_values_list = [parameter_values_] * len(gradient_circuit) - job = self._sampler.run( - gradient_circuit, gradient_parameter_values_list, **run_options - ) + job = self._sampler.run(gradient_circuit, gradient_parameter_values_list, **run_options) results = job.result() param_set = set(parameters) @@ -113,8 +111,6 @@ def _evaluate( sign, k2 = divmod(k, num_bitstrings) dists[i][k2] += (-1) ** sign * bound_coeff * v - gradients.append( - [QuasiDistribution(dist) for dist in dists] - ) + gradients.append([QuasiDistribution(dist) for dist in dists]) status.append(job.status()) return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 2136d9b76482..803dc234630e 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -21,7 +21,7 @@ from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.opflow import PauliSumOp -from qiskit.primitives import BaseEstimator, EstimatorResult +from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -36,11 +36,7 @@ class ParamShiftEstimatorGradient(BaseEstimatorGradient): """Parameter shift estimator gradient""" - def __init__( - self, - estimator: BaseEstimator, - **run_options - ): + def __init__(self, estimator: BaseEstimator, **run_options): """ Args: estimator: The estimator used to compute the gradients. @@ -67,7 +63,8 @@ def _evaluate( if circ_index is not None: circuit_index = circ_index else: - # if the given circuit is a new one, make gradient circuit data and base parameter values + # if the given circuit is a new one, make gradient circuit data and + # base parameter values. circuit_index = len(self._circuits) self._circuit_ids[id(circuit)] = circuit_index self._gradient_circuit_data_dict[ diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index dd954878bec5..5c013e5e66dc 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -21,7 +21,7 @@ import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.primitives import BaseSampler from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient @@ -57,7 +57,8 @@ def _evaluate( if index is not None: circuit_index = index else: - # if the given circuit is a new one, make gradient circuit data and base parameter values + # if the given circuit is a new one, make gradient circuit data and + # base parameter values circuit_index = len(self._circuits) self._circuit_ids[id(circuit)] = circuit_index self._gradient_circuit_data_dict[ @@ -152,9 +153,7 @@ def _evaluate( } ) ) - gradients.append( - [QuasiDistribution(dist) for dist in dists] - ) + gradients.append([QuasiDistribution(dist) for dist in dists]) status.append(job.status()) - return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) + return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index 5949194915a1..9b96789e714a 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -33,6 +33,7 @@ class SamplerGradientResult: sampling probability for the j-th parameter in ``circuits[i]``. status: List of JobStatus for each SamplerResult. """ + quasi_dists: list[list[QuasiDistribution]] status: list[JobStatus] metadata: list[dict[str, Any]] diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 521f55b0d4b1..b87602c1c425 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -9,6 +9,9 @@ # 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. + +# pylint: disable=invalid-name + """ Utility functions for gradients """ @@ -86,7 +89,22 @@ def make_param_shift_gradient_circuit_data( necessary data to calculate gradients with the parameter shift method. """ - supported_gates = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz", "ryy", "rxx", "rzz"] + supported_gates = [ + "x", + "y", + "z", + "h", + "rx", + "ry", + "rz", + "p", + "cx", + "cy", + "cz", + "ryy", + "rxx", + "rzz", + ] circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) g_circuit = circuit2.copy_empty_like(f"g_{circuit2.name}") @@ -303,6 +321,7 @@ def make_lin_comb_gradient_circuit( def _gate_gradient(gate: Gate) -> Instruction: """Returns the derivative of the gate""" + # pylint: disable=too-many-return-statements if isinstance(gate, RXGate): # theta return CXGate() diff --git a/releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml b/releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml new file mode 100644 index 000000000000..fc304ae64e41 --- /dev/null +++ b/releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + New gradient classes using the primitives has been added. They internally + use the primitives to calculate the gradients. There are 3 types of + gradient classes (Finite Difference, Parameter Shift, and + Linear Combination of Unitary) for a sampler and estimator. + + Example:: + .. code-block:: python + + estimator = Estimator(...) + gradient = ParamShiftEstimatorGradient(estimator) + results = gradient.evaluate(circuits, observables, parameters) diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index d3184180bae8..2d911c1d175e 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -213,7 +213,9 @@ def test_gradient_parameter_coefficient(self, grad): values = gradient.evaluate([qc], [op], [param]).values[0] np.testing.assert_almost_equal(values, correct_results[i]) - @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) def test_gradient_partial(self, grad): """Test the estimator gradient for partial""" estimator = Estimator() @@ -232,7 +234,9 @@ def test_gradient_partial(self, grad): values = gradient.evaluate([qc], [op], [param]).values[0] np.testing.assert_almost_equal(values, correct_results[i]) - @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) def test_gradient_multi_arguments(self, grad): """Test the estimator gradient for multiple arguments""" estimator = Estimator() @@ -249,7 +253,7 @@ def test_gradient_multi_arguments(self, grad): [-1], ] op = SparsePauliOp.from_list([("Z", 1)]) - values = gradient.evaluate([qc,qc2], [op]*2, param_list).values + values = gradient.evaluate([qc, qc2], [op] * 2, param_list).values np.testing.assert_almost_equal(values, correct_results) c = Parameter("c") @@ -259,13 +263,15 @@ def test_gradient_multi_arguments(self, grad): param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] correct_results2 = [ [-0.70710678], - [-0.70710678 if p==c else 0 for p in qc3.parameters], + [-0.70710678 if p == c else 0 for p in qc3.parameters], ] - values2 = gradient.evaluate([qc,qc3], [op]*2, param_list2, partial=[[a],[c]]).values + values2 = gradient.evaluate([qc, qc3], [op] * 2, param_list2, partial=[[a], [c]]).values np.testing.assert_almost_equal(values2[0], correct_results2[0]) np.testing.assert_almost_equal(values2[1], correct_results2[1]) - @combine(grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) def test_gradient_validation(self, grad): """Test estimator gradient's validation""" estimator = Estimator() @@ -282,7 +288,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc, qc], [op], param_list, partial=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc], [op], [[np.pi / 4,np.pi / 4]]) + gradient.evaluate([qc], [op], [[np.pi / 4, np.pi / 4]]) if __name__ == "__main__": diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 1878f2afd5ff..0f875b809018 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -20,12 +20,9 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients.finite_diff_sampler_gradient import \ - FiniteDiffSamplerGradient -from qiskit.algorithms.gradients.lin_comb_sampler_gradient import \ - LinCombSamplerGradient -from qiskit.algorithms.gradients.param_shift_sampler_gradient import \ - ParamShiftSamplerGradient +from qiskit.algorithms.gradients.finite_diff_sampler_gradient import FiniteDiffSamplerGradient +from qiskit.algorithms.gradients.lin_comb_sampler_gradient import LinCombSamplerGradient +from qiskit.algorithms.gradients.param_shift_sampler_gradient import ParamShiftSamplerGradient from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.exceptions import QiskitError @@ -350,7 +347,7 @@ def test_gradient_partial(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param],partial=[[a]]).quasi_dists[0] + quasi_dists = gradient.evaluate([qc], [param], partial=[[a]]).quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): if correct_results[i][j]: for k in quasi_dist: @@ -376,7 +373,7 @@ def test_gradient_multi_arguments(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], [{0: -0.499999, 1: 0.499999}], ] - quasi_dists = gradient.evaluate([qc,qc2], param_list).quasi_dists + quasi_dists = gradient.evaluate([qc, qc2], param_list).quasi_dists for j, q_dists in enumerate(quasi_dists): quasi_dist = q_dists[0] for k in quasi_dist: @@ -388,10 +385,10 @@ def test_gradient_multi_arguments(self, grad): qc3.rz(a, 0) qc3.measure_all() param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] - quasi_dists = gradient.evaluate([qc,qc3], param_list2, partial=[[a],[c]]).quasi_dists + quasi_dists = gradient.evaluate([qc, qc3], param_list2, partial=[[a], [c]]).quasi_dists correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)} if p==c else {} for p in qc3.parameters] + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)} if p == c else {} for p in qc3.parameters], ] for i, result in enumerate(quasi_dists): for j, q_dists in enumerate(result): @@ -416,7 +413,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc, qc], param_list, partial=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc], [[np.pi / 4,np.pi / 4]]) + gradient.evaluate([qc], [[np.pi / 4, np.pi / 4]]) if __name__ == "__main__": From 9ebe3ef22ec42fee90bebc29d5814f65dd380b6a Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 24 Aug 2022 10:39:16 +0900 Subject: [PATCH 05/37] fix based on the comments Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/__init__.py | 4 +- .../gradients/base_estimator_gradient.py | 41 ++++++++++++------- .../gradients/base_sampler_gradient.py | 39 ++++++++++++------ .../gradients/estimator_gradient_result.py | 9 +--- .../finite_diff_estimator_gradient.py | 21 +++++----- .../gradients/finite_diff_sampler_gradient.py | 19 +++++---- 6 files changed, 78 insertions(+), 55 deletions(-) diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py index 487a5bd96e4b..6ac3e6cf2a9f 100644 --- a/qiskit/algorithms/gradients/__init__.py +++ b/qiskit/algorithms/gradients/__init__.py @@ -45,8 +45,8 @@ .. autosummary:: :toctree: ../stubs/ - EstimatorResult - SamplerResult + EstimatorGradientResult + SamplerGradientResult """ from .base_estimator_gradient import BaseEstimatorGradient diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index 64c0e621fd77..e0750b0f1c0d 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -17,7 +17,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Sequence +from collections.abc import Sequence, Mapping +from copy import copy from qiskit.circuit import Parameter, QuantumCircuit from qiskit.exceptions import QiskitError @@ -39,10 +40,17 @@ def __init__( """ Args: estimator: The estimator used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._estimator: BaseEstimator = estimator - self._circuits: Sequence[QuantumCircuit] = [] - self._circuit_ids: dict[int, int] = {} + self._circuits: Sequence[QuantumCircuit] | None = None + if self._circuits is None: + self._circuits = [] + self._circuit_ids: Mapping[int, int] | None = None + if self._circuit_ids is None: + self._circuit_ids = {} self._default_run_options = run_options def evaluate( @@ -50,7 +58,7 @@ def evaluate( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: """Run the job of the gradients of expectation values. @@ -59,9 +67,13 @@ def evaluate( circuits: The list of quantum circuits to compute the gradients. observables: The list of observables. parameter_values: The list of parameter values to be bound to the circuit. - partial: The list of Parameters to calculate only the gradients of the specified parameters. - Defaults to None, which means that the gradients of all parameters will be calculated. - run_options: Backend runtime options used for circuit execution. + parameters: The Sequence of Sequence of Parameters to calculate only the gradients of + the specified parameters. Each Sequence of Parameters corresponds to a circuit in + `circuits`. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. Returns: The job object of the gradients of the expectation values. The i-th result corresponds to @@ -80,14 +92,14 @@ def evaluate( if len(circuits) != len(observables): raise QiskitError( f"The number of circuits ({len(circuits)}) does not match " - f"the number of observables ({len(partial)})." + f"the number of observables ({len(observables)})." ) - if partial is not None: - if len(circuits) != len(partial): + if parameters is not None: + if len(circuits) != len(parameters): raise QiskitError( f"The number of circuits ({len(circuits)}) does not match " - f"the number of partial parameter sets ({len(partial)})." + f"the number of the specified parameter sets ({len(parameters)})." ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): @@ -107,8 +119,9 @@ def evaluate( # The priority of run option is as follows: # run_options in `run` method > gradient's default run_options > primitive's default setting. - run_options = run_options or self._default_run_options - return self._evaluate(circuits, observables, parameter_values, partial, **run_options) + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + return self._evaluate(circuits, observables, parameter_values, parameters, **run_opts) @abstractmethod def _evaluate( @@ -116,7 +129,7 @@ def _evaluate( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter]] | None = None, **run_options, ) -> EstimatorGradientResult: raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 4532f0ac96c0..526e9e20b47d 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -17,7 +17,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Sequence +from collections.abc import Sequence, Mapping +from copy import copy from qiskit.circuit import QuantumCircuit, Parameter from qiskit.exceptions import QiskitError @@ -32,17 +33,24 @@ def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._sampler: BaseSampler = sampler - self._circuits: list[QuantumCircuit] = [] - self._circuit_ids: dict[int, int] = {} + self._circuits: Sequence[QuantumCircuit] | None = None + if self._circuits is None: + self._circuits = [] + self._circuit_ids: Mapping[int, int] | None = None + if self._circuit_ids is None: + self._circuit_ids = {} self._default_run_options = run_options def evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: """Run the job of the gradients of the sampling probability. @@ -50,9 +58,13 @@ def evaluate( Args: circuits: The list of quantum circuits to compute the gradients. parameter_values: The list of parameter values to be bound to the circuit. - partial: The list of Parameters to calculate only the gradients of the specified parameters. - Defaults to None, which means that the gradients of all parameters will be calculated. - run_options: Backend runtime options used for circuit execution. + parameters: The Sequence of Sequence of Parameters to calculate only the gradients of + the specified parameters. Each Sequence of Parameters corresponds to a circuit in + `circuits`. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. Returns: The job object of the gradients of the sampling probability. The i-th result corresponds to @@ -69,11 +81,11 @@ def evaluate( f"The number of circuits ({len(circuits)}) does not match " f"the number of parameter value sets ({len(parameter_values)})." ) - if partial is not None: - if len(circuits) != len(partial): + if parameters is not None: + if len(circuits) != len(parameters): raise QiskitError( f"The number of circuits ({len(circuits)}) does not match " - f"the number of partial parameter sets ({len(partial)})." + f"the number of the specified parameter sets ({len(parameters)})." ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): @@ -85,15 +97,16 @@ def evaluate( # The priority of run option is as follows: # run_options in `run` method > gradient's default run_options > primitive's default run_options. - run_options = run_options or self._default_run_options - return self._evaluate(circuits, parameter_values, partial, **run_options) + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + return self._evaluate(circuits, parameter_values, parameters, **run_opts) @abstractmethod def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter]] | None = None, **run_options, ) -> SamplerGradientResult: raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index e813f19c2d26..59aaea523041 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -20,20 +20,15 @@ import numpy as np -from qiskit.providers import JobStatus - @dataclass(frozen=True) class EstimatorGradientResult: """Result of EstimatorGradient. Args: - results (list[EstimatorResult]): List of EstimatorResults. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th value - in the i-th result corresponds to the gradients of the j-th parameter in ``circuits[i]``. - status: List of JobStatus for each EstimatorResult. + values: The gradients of the expectation values. + metadata: Additional information about the job. """ values: list[np.ndarray] - status: list[JobStatus] metadata: list[dict[str, Any]] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 340fba1bf2a3..c9d9c669ef6a 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -38,6 +38,9 @@ def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_option Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._epsilon = epsilon self._base_parameter_values_dict = {} @@ -48,14 +51,13 @@ def _evaluate( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, observable, parameter_values_, partial_ in zip( - circuits, observables, parameter_values, partial + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters ): index = self._circuit_ids.get(id(circuit)) if index is not None: @@ -73,13 +75,13 @@ def _evaluate( base_parameter_values_list = [] gradient_parameter_values = np.zeros(len(circuit_parameters)) - # a parameter set for the partial option - parameters = partial_ or circuit_parameters + # a parameter set for the parameter option + parameters = parameters_ or circuit_parameters param_set = set(parameters) result_index = 0 result_index_map = {} - # bring the base parameter values for parameters only in the partial parameter set. + # bring the base parameter values for parameters only in the specified parameter set. for i, param in enumerate(circuit_parameters): gradient_parameter_values[i] = parameter_values_[i] if param in param_set: @@ -118,5 +120,4 @@ def _evaluate( values[i] -= results.values[result_index_map[param] * 2 + 1] / (2 * self._epsilon) gradients.append(values) - status.append(job.status()) - return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index f299bb3ee80d..2a49686e7b93 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -41,6 +41,9 @@ def __init__( Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._epsilon = epsilon @@ -51,13 +54,12 @@ def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): index = self._circuit_ids.get(id(circuit)) if index is not None: circuit_index = index @@ -74,13 +76,13 @@ def _evaluate( base_parameter_values_list = [] gradient_parameter_values = np.zeros(len(circuit_parameters)) - # a parameter set for the partial option - parameters = partial_ or circuit_parameters + # a parameter set for the parameter option + parameters = parameters_ or circuit_parameters param_set = set(parameters) result_index = 0 result_index_map = {} - # bring the base parameter values for parameters only in the partial parameter set. + # bring the base parameter values for parameters only in the specified parameter set. for i, param in enumerate(circuit_parameters): gradient_parameter_values[i] = parameter_values_[i] if param in param_set: @@ -132,5 +134,4 @@ def _evaluate( ) gradients.append([QuasiDistribution(dist) for dist in dists]) - status.append(job.status()) - return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) + return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) From aeb76ffdee85bd57539364dbf290f8fcaf8c9ddb Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 24 Aug 2022 10:40:09 +0900 Subject: [PATCH 06/37] fix Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/lin_comb_estimator_gradient.py | 19 +++++++++-------- .../gradients/lin_comb_sampler_gradient.py | 17 ++++++++------- .../param_shift_estimator_gradient.py | 21 ++++++++++--------- .../gradients/param_shift_sampler_gradient.py | 19 +++++++++-------- .../gradients/sampler_gradient_result.py | 9 ++------ .../algorithms/test_estimator_gradient.py | 20 +++++++++++------- .../algorithms/test_sampler_gradient.py | 19 ++++++++++------- 7 files changed, 65 insertions(+), 59 deletions(-) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index b7ac3571b199..7b0cc065e5d5 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -43,6 +43,9 @@ def __init__(self, estimator: BaseEstimator, **run_options): """ Args: estimator: The estimator used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuit_data_dict = {} super().__init__(estimator, **run_options) @@ -52,15 +55,14 @@ def _evaluate( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, observable, parameter_values_, partial_ in zip( - circuits, observables, parameter_values, partial + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters ): index = self._circuit_ids.get(id(circuit)) if index is not None: @@ -80,8 +82,8 @@ def _evaluate( circuit_parameters = self._circuits[circuit_index].parameters parameter_value_map = {} - # a parameter set for the partial option - parameters = partial_ or self._circuits[circuit_index].parameters + # a parameter set for the parameters option + parameters = parameters_ or self._circuits[circuit_index].parameters param_set = set(parameters) result_index = 0 @@ -122,5 +124,4 @@ def _evaluate( values[i] += bound_coeff * results.values[result_index_map[param][j]] gradients.append(values) - status.append(job.status()) - return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 3b95252b99f8..bbd6c775213c 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -37,6 +37,9 @@ def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuit_data_dict = {} @@ -46,14 +49,13 @@ def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): index = self._circuit_ids.get(id(circuit)) if index is not None: circuit_index = index @@ -70,8 +72,8 @@ def _evaluate( circuit_parameters = self._circuits[circuit_index].parameters parameter_value_map = {} - # a parameter set for the partial option - parameters = partial_ or self._circuits[circuit_index].parameters + # a parameter set for the parameter option + parameters = parameters_ or self._circuits[circuit_index].parameters param_set = set(parameters) result_index = 0 @@ -112,5 +114,4 @@ def _evaluate( dists[i][k2] += (-1) ** sign * bound_coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) - status.append(job.status()) - return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) + return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 803dc234630e..0a20c36b78fb 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -40,6 +40,9 @@ def __init__(self, estimator: BaseEstimator, **run_options): """ Args: estimator: The estimator used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuit_data_dict = {} self._base_parameter_values_dict = {} @@ -50,14 +53,13 @@ def _evaluate( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, observable, parameter_values_, partial_ in zip( - circuits, observables, parameter_values, partial + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters ): circ_index = self._circuit_ids.get(id(circuit)) if circ_index is not None: @@ -87,13 +89,13 @@ def _evaluate( len(gradient_circuit_data.gradient_circuit.parameters) ) - # a parameter set for the partial option - parameters = partial_ or self._circuits[circuit_index].parameters + # a parameter set for the parameters option + parameters = parameters_ or self._circuits[circuit_index].parameters param_set = set(parameters) result_index = 0 result_index_map = {} - # bring the base parameter values for parameters only in the partial parameter set. + # bring the base parameter values for parameters only in the specified parameter set. for i, param in enumerate(circuit_parameters): parameter_value_map[param] = parameter_values_[i] for g_param in gradient_parameter_map[param]: @@ -144,5 +146,4 @@ def _evaluate( # minus values[i] -= bound_coeff * results.values[result_index_map[g_param] * 2 + 1] gradients.append(values) - status.append(job.status()) - return EstimatorGradientResult(values=gradients, status=status, metadata=run_options) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 5c013e5e66dc..436b0fb044c6 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -36,6 +36,9 @@ def __init__(self, sampler: BaseSampler, **run_options): """ Args: sampler: The sampler used to compute the gradients. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuit_data_dict = {} self._base_parameter_values_dict = {} @@ -45,14 +48,13 @@ def _evaluate( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - partial: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter]] | None = None, **run_options, ) -> SamplerGradientResult: - partial = partial or [[] for _ in range(len(circuits))] + parameters = parameters or [None for _ in range(len(circuits))] gradients = [] - status = [] - for circuit, parameter_values_, partial_ in zip(circuits, parameter_values, partial): + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): index = self._circuit_ids.get(id(circuit)) if index is not None: circuit_index = index @@ -81,13 +83,13 @@ def _evaluate( len(gradient_circuit_data.gradient_circuit.parameters) ) - # a parameter set for the partial option - parameters = partial_ or self._circuits[circuit_index].parameters + # a parameter set for the parameter option + parameters = parameters_ or self._circuits[circuit_index].parameters param_set = set(parameters) result_index = 0 result_index_map = {} - # bring the base parameter values for parameters only in the partial parameter set. + # bring the base parameter values for parameters only in the specified parameter set. for i, param in enumerate(circuit_parameters): parameter_value_map[param] = parameter_values_[i] for g_param in gradient_parameter_map[param]: @@ -155,5 +157,4 @@ def _evaluate( ) gradients.append([QuasiDistribution(dist) for dist in dists]) - status.append(job.status()) - return SamplerGradientResult(quasi_dists=gradients, status=status, metadata=run_options) + return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index 9b96789e714a..d11286f9fca0 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -19,7 +19,6 @@ from dataclasses import dataclass from qiskit.result import QuasiDistribution -from qiskit.providers import JobStatus @dataclass(frozen=True) @@ -27,13 +26,9 @@ class SamplerGradientResult: """Result of SamplerGradient. Args: - results (list[SamplerResult]): List of SamplerResults. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th - quasi-probability distribution in the i-th result corresponds to the gradients of the - sampling probability for the j-th parameter in ``circuits[i]``. - status: List of JobStatus for each SamplerResult. + quasi_dists: The gradients of the quasi distributions. + metadata: Additional information about the job. """ quasi_dists: list[list[QuasiDistribution]] - status: list[JobStatus] metadata: list[dict[str, Any]] diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 2d911c1d175e..999557e461cf 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -216,8 +216,8 @@ def test_gradient_parameter_coefficient(self, grad): @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] ) - def test_gradient_partial(self, grad): - """Test the estimator gradient for partial""" + def test_gradient_parameters(self, grad): + """Test the estimator gradient for parameters""" estimator = Estimator() a = Parameter("a") b = Parameter("b") @@ -259,15 +259,19 @@ def test_gradient_multi_arguments(self, grad): c = Parameter("c") qc3 = QuantumCircuit(1) qc3.rx(c, 0) - qc3.rz(a, 0) - param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] + qc3.ry(a, 0) + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] correct_results2 = [ [-0.70710678], - [-0.70710678 if p == c else 0 for p in qc3.parameters], + [-0.5 if p == c else 0 for p in qc3.parameters], + [-0.5, -0.5], ] - values2 = gradient.evaluate([qc, qc3], [op] * 2, param_list2, partial=[[a], [c]]).values + values2 = gradient.evaluate( + [qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None] + ).values np.testing.assert_almost_equal(values2[0], correct_results2[0]) np.testing.assert_almost_equal(values2[1], correct_results2[1]) + np.testing.assert_almost_equal(values2[2], correct_results2[2]) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -284,9 +288,9 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc], [op], param_list) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], [op, op], param_list, partial=[[a]]) + gradient.evaluate([qc, qc], [op, op], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], [op], param_list, partial=[[a]]) + gradient.evaluate([qc, qc], [op], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): gradient.evaluate([qc], [op], [[np.pi / 4, np.pi / 4]]) diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 0f875b809018..3ce0bbd82d70 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -332,8 +332,8 @@ def test_gradient_parameter_coefficient(self, grad): self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) - def test_gradient_partial(self, grad): - """Test the sampler gradient for partial""" + def test_gradient_parameters(self, grad): + """Test the sampler gradient for parameters""" sampler = Sampler() a = Parameter("a") b = Parameter("b") @@ -347,7 +347,7 @@ def test_gradient_partial(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param], partial=[[a]]).quasi_dists[0] + quasi_dists = gradient.evaluate([qc], [param], parameters=[[a]]).quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): if correct_results[i][j]: for k in quasi_dist: @@ -382,13 +382,16 @@ def test_gradient_multi_arguments(self, grad): c = Parameter("c") qc3 = QuantumCircuit(1) qc3.rx(c, 0) - qc3.rz(a, 0) + qc3.ry(a, 0) qc3.measure_all() - param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4]] - quasi_dists = gradient.evaluate([qc, qc3], param_list2, partial=[[a], [c]]).quasi_dists + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] + quasi_dists = gradient.evaluate( + [qc, qc3, qc3], param_list2, parameters=[[a], [c], None] + ).quasi_dists correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)} if p == c else {} for p in qc3.parameters], + [{0: -0.25, 1: 0.25} if p == c else {} for p in qc3.parameters], + [{0: -0.25, 1: 0.25}, {0: -0.25, 1: 0.25}], ] for i, result in enumerate(quasi_dists): for j, q_dists in enumerate(result): @@ -411,7 +414,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc], param_list) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], param_list, partial=[[a]]) + gradient.evaluate([qc, qc], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): gradient.evaluate([qc], [[np.pi / 4, np.pi / 4]]) From f19222dbf87ddf82b54d527cb31e26e8143c5cb5 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 24 Aug 2022 17:13:57 +0900 Subject: [PATCH 07/37] add spsa gradient Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/__init__.py | 6 + .../finite_diff_estimator_gradient.py | 4 +- .../gradients/finite_diff_sampler_gradient.py | 4 +- .../gradients/lin_comb_estimator_gradient.py | 4 +- .../gradients/lin_comb_sampler_gradient.py | 4 +- .../param_shift_estimator_gradient.py | 9 +- .../gradients/param_shift_sampler_gradient.py | 8 +- .../gradients/spsa_estimator_gradient.py | 103 ++++++++++++++++ .../gradients/spsa_sampler_gradient.py | 114 ++++++++++++++++++ qiskit/algorithms/gradients/utils.py | 27 +++++ .../algorithms/test_estimator_gradient.py | 39 +++++- .../algorithms/test_sampler_gradient.py | 61 +++++++++- 12 files changed, 366 insertions(+), 17 deletions(-) create mode 100644 qiskit/algorithms/gradients/spsa_estimator_gradient.py create mode 100644 qiskit/algorithms/gradients/spsa_sampler_gradient.py diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py index 6ac3e6cf2a9f..6b34a73704eb 100644 --- a/qiskit/algorithms/gradients/__init__.py +++ b/qiskit/algorithms/gradients/__init__.py @@ -38,6 +38,8 @@ LinCombSamplerGradient ParamShiftEstimatorGradient ParamShiftSamplerGradient + SPSAEstimatorGradient + SPSASamplerGradient Results ======= @@ -59,6 +61,8 @@ from .param_shift_estimator_gradient import ParamShiftEstimatorGradient from .param_shift_sampler_gradient import ParamShiftSamplerGradient from .sampler_gradient_result import SamplerGradientResult +from .spsa_estimator_gradient import SPSAEstimatorGradient +from .spsa_sampler_gradient import SPSASamplerGradient __all__ = [ "BaseEstimatorGradient", @@ -71,4 +75,6 @@ "ParamShiftEstimatorGradient", "ParamShiftSamplerGradient", "SamplerGradientResult", + "SPSAEstimatorGradient", + "SPSASamplerGradient", ] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index c9d9c669ef6a..2879480a9976 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -43,7 +43,9 @@ def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_option setting. Higher priority setting overrides lower priority setting. """ self._epsilon = epsilon - self._base_parameter_values_dict = {} + self._base_parameter_values_dict = None + if self._base_parameter_values_dict is None: + self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 2a49686e7b93..d80daa8d559c 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -47,7 +47,9 @@ def __init__( """ self._epsilon = epsilon - self._base_parameter_values_dict = {} + self._base_parameter_values_dict = None + if self._base_parameter_values_dict is None: + self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 7b0cc065e5d5..6430e63f2c41 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -47,7 +47,9 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} + self._gradient_circuit_data_dict = None + if self._gradient_circuit_data_dict is None: + self._gradient_circuit_data_dict = {} super().__init__(estimator, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index bbd6c775213c..bd60685a4c3b 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -42,7 +42,9 @@ def __init__(self, sampler: BaseSampler, **run_options): setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} + self._gradient_circuit_data_dict = None + if self._gradient_circuit_data_dict is None: + self._gradient_circuit_data_dict = {} super().__init__(sampler, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 0a20c36b78fb..1725de0cf330 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -44,8 +44,13 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} - self._base_parameter_values_dict = {} + self._gradient_circuit_data_dict = None + if self._gradient_circuit_data_dict is None: + self._gradient_circuit_data_dict = {} + + self._base_parameter_values_dict = None + if self._base_parameter_values_dict is None: + self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 436b0fb044c6..49b476dc6654 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -40,8 +40,12 @@ def __init__(self, sampler: BaseSampler, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} - self._base_parameter_values_dict = {} + self._gradient_circuit_data_dict = None + if self._gradient_circuit_data_dict is None: + self._gradient_circuit_data_dict = {} + self._base_parameter_values_dict = None + if self._base_parameter_values_dict is None: + self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) def _evaluate( diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py new file mode 100644 index 000000000000..98a2925c5def --- /dev/null +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from typing import Sequence +import random + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.opflow import PauliSumOp +from qiskit.primitives import BaseEstimator +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .base_estimator_gradient import BaseEstimatorGradient +from .estimator_gradient_result import EstimatorGradientResult +from .utils import make_spsa_base_parameter_values + + +class SPSAEstimatorGradient(BaseEstimatorGradient): + """ + Gradient of Estimator with the Simultaneous Perturbation Stochastic Approximation (SPSA). + """ + + def __init__( + self, + estimator: BaseEstimator, + epsilon: float = 1e-6, + seed: int | None = None, + **run_options, + ): + """ + Args: + estimator: The estimator used to compute the gradients. + epsilon: The offset size for the finite difference gradients. + seed: The seed for a random perturbation vector. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting.""" + self._epsilon = epsilon + self._seed = random.seed(seed) if seed else None + + super().__init__(estimator, **run_options) + + def _evaluate( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **run_options, + ) -> EstimatorGradientResult: + parameters = parameters or [None for _ in range(len(circuits))] + gradients = [] + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters + ): + + base_parameter_values_list = make_spsa_base_parameter_values(circuit, self._epsilon) + circuit_parameters = circuit.parameters + gradient_parameter_values = np.zeros(len(circuit_parameters)) + + # a parameter set for the parameter option + parameters = parameters_ or circuit_parameters + param_set = set(parameters) + + gradient_parameter_values = np.array(parameter_values_) + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + gradient_circuits = [circuit] * len(gradient_parameter_values_list) + observable_list = [observable] * len(gradient_parameter_values_list) + job = self._estimator.run( + gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + ) + results = job.result() + # Combines the results and coefficients to reconstruct the gradient + # for the original circuit parameters + values = np.zeros(len(parameter_values_)) + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + # plus + values[i] += results.values[0] / (2 * base_parameter_values_list[0][i]) + # minus + values[i] -= results.values[1] / (2 * base_parameter_values_list[0][i]) + + gradients.append(values) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py new file mode 100644 index 000000000000..cfd153a027b5 --- /dev/null +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -0,0 +1,114 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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 +# 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. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from collections import Counter +from typing import Sequence +import random + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.result import QuasiDistribution + +from .base_sampler_gradient import BaseSamplerGradient +from .sampler_gradient_result import SamplerGradientResult +from .utils import make_spsa_base_parameter_values + + +class SPSASamplerGradient(BaseSamplerGradient): + """ + Gradient of Sampler with the Simultaneous Perturbation Stochastic Approximation (SPSA). + """ + + def __init__( + self, + sampler: BaseSampler, + epsilon: float = 1e-6, + seed: int | None = None, + **run_options, + ): + """ + Args: + sampler: The sampler used to compute the gradients. + epsilon: The offset size for the finite difference gradients. + seed: The seed for a random perturbation vector. + run_options: Backend runtime options used for circuit execution. The order of priority is: + run_options in `run` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting.""" + self._epsilon = epsilon + self._seed = random.seed(seed) if seed else None + + super().__init__(sampler, **run_options) + + def _evaluate( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **run_options, + ) -> SamplerGradientResult: + parameters = parameters or [None for _ in range(len(circuits))] + gradients = [] + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + + base_parameter_values_list = make_spsa_base_parameter_values(circuit, self._epsilon) + circuit_parameters = circuit.parameters + gradient_parameter_values = np.zeros(len(circuit_parameters)) + + # a parameter set for the parameter option + parameters = parameters_ or circuit_parameters + param_set = set(parameters) + + gradient_parameter_values = np.array(parameter_values_) + # add the given parameter values and the base parameter values + gradient_parameter_values_list = [ + gradient_parameter_values + base_parameter_values + for base_parameter_values in base_parameter_values_list + ] + gradient_circuits = [circuit] * len(gradient_parameter_values_list) + job = self._sampler.run( + gradient_circuits, gradient_parameter_values_list, **run_options + ) + results = job.result() + # Combines the results and coefficients to reconstruct the gradient values + # for the original circuit parameters + dists = [Counter() for _ in range(len(parameter_values_))] + for i, param in enumerate(circuit_parameters): + if param not in param_set: + continue + # plus + dists[i].update( + Counter( + { + k: v / (2 * base_parameter_values_list[0][i]) + for k, v in results.quasi_dists[0].items() + } + ) + ) + # minus + dists[i].update( + Counter( + { + k: -1 * v / (2 * base_parameter_values_list[0][i]) + for k, v in results.quasi_dists[1].items() + } + ) + ) + + gradients.append([QuasiDistribution(dist) for dist in dists]) + + return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index b87602c1c425..181c6a76cb24 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -21,6 +21,7 @@ from collections import defaultdict from copy import deepcopy from dataclasses import dataclass +import random from typing import Dict, List import numpy as np @@ -360,3 +361,29 @@ def _gate_gradient(gate: Gate) -> Instruction: czx = czx_circ.to_instruction() return czx raise TypeError(f"Unrecognized parameterized gate, {gate}") + + +def make_spsa_base_parameter_values( + circuit: QuantumCircuit, epsilon: float = 1e-6 +) -> List[np.ndarray]: + """Makes base parameter values for the SPSA. Each base parameter value will + be added to the given parameter values in later calculations. + + Args: + circuit: circuit for the base parameter values. + epsilon: The offset size for the finite difference gradients. + + Returns: + List: The base parameter values for the SPSA. + """ + + base_parameter_values = [] + # Make a perturbation vector + parameter_values_plus = np.array( + [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] + ) + parameter_values_plus = epsilon * parameter_values_plus + parameter_values_minus = -parameter_values_plus + base_parameter_values.append(parameter_values_plus) + base_parameter_values.append(parameter_values_minus) + return base_parameter_values diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 999557e461cf..db0399134c8a 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -20,9 +20,12 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients.finite_diff_estimator_gradient import FiniteDiffEstimatorGradient -from qiskit.algorithms.gradients.lin_comb_estimator_gradient import LinCombEstimatorGradient -from qiskit.algorithms.gradients.param_shift_estimator_gradient import ParamShiftEstimatorGradient +from qiskit.algorithms.gradients import ( + FiniteDiffEstimatorGradient, + ParamShiftEstimatorGradient, + LinCombEstimatorGradient, + SPSAEstimatorGradient, +) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.exceptions import QiskitError @@ -223,15 +226,15 @@ def test_gradient_parameters(self, grad): b = Parameter("b") qc = QuantumCircuit(1) qc.rx(a, 0) - qc.rz(b, 0) + qc.rx(b, 0) gradient = grad(estimator) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ - [-0.70710678, 0], + [-0.70710678 if p == a else 0 for p in qc.parameters], ] op = SparsePauliOp.from_list([("Z", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] + values = gradient.evaluate([qc], [op], [param], parameters=[[a]]).values[0] np.testing.assert_almost_equal(values, correct_results[i]) @combine( @@ -294,6 +297,30 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc], [op], [[np.pi / 4, np.pi / 4]]) + def test_spsa_gradient(self): + """Test the SPSA estimator gradient""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(2) + qc.rx(b, 0) + qc.rx(a, 1) + param_list = [[1, 1]] + correct_results = [[-0.84147098, 0.84147098]] + op = SparsePauliOp.from_list([("ZI", 1)]) + gradient = SPSAEstimatorGradient(estimator, seed=123) + values = gradient.evaluate([qc], [op], param_list).values + np.testing.assert_almost_equal(values, correct_results) + + # multi parameters + gradient = SPSAEstimatorGradient(estimator, seed=123) + param_list2 = [[1, 1], [1, 1], [3, 3]] + values2 = gradient.evaluate( + [qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None] + ).values + correct_results2 = [[-0.84147098, 0.84147098], [0.0, 0.84147098], [-0.14112001, 0.14112001]] + np.testing.assert_almost_equal(values2, correct_results2) + if __name__ == "__main__": unittest.main() diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 3ce0bbd82d70..872bd6d19d03 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -20,9 +20,12 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients.finite_diff_sampler_gradient import FiniteDiffSamplerGradient -from qiskit.algorithms.gradients.lin_comb_sampler_gradient import LinCombSamplerGradient -from qiskit.algorithms.gradients.param_shift_sampler_gradient import ParamShiftSamplerGradient +from qiskit.algorithms.gradients import ( + FiniteDiffSamplerGradient, + LinCombSamplerGradient, + ParamShiftSamplerGradient, + SPSASamplerGradient, +) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.exceptions import QiskitError @@ -418,6 +421,58 @@ def test_gradient_validation(self, grad): with self.assertRaises(QiskitError): gradient.evaluate([qc], [[np.pi / 4, np.pi / 4]]) + def test_spsa_gradient(self): + """Test the SPSA sampler gradient""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(2) + qc.rx(b, 0) + qc.rx(a, 1) + qc.measure_all() + param_list = [[1, 2]] + correct_results = [ + [ + {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, + ], + ] + gradient = SPSASamplerGradient(sampler, seed=123) + # quasi_dists = gradient.evaluate([qc], param_list).quasi_dists + for i, param in enumerate(param_list): + quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + for j, quasi_dist in enumerate(quasi_dists): + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + # multi parameters + param_list2 = [[1, 2], [1, 2], [3, 4]] + correct_results2 = [ + [ + {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, + ], + [ + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111} if p == b else {} + for p in qc.parameters + ], + [ + {0: -0.0141129, 1: -0.0564471, 2: -0.3642884, 3: 0.4348484}, + {0: 0.0141129, 1: 0.0564471, 2: 0.3642884, 3: -0.4348484}, + ], + ] + gradient = SPSASamplerGradient(sampler, seed=123) + quasi_dists = gradient.evaluate( + [qc] * 3, param_list2, parameters=[None, [b], None] + ).quasi_dists + + for i, result in enumerate(quasi_dists): + for j, q_dists in enumerate(result): + if correct_results2[i][j]: + for k in q_dists: + self.assertAlmostEqual(q_dists[k], correct_results2[i][j][k], 3) + else: + self.assertEqual(q_dists, {}) + if __name__ == "__main__": unittest.main() From 8c63899c4d05ecad371eb6d95ba1d24d027a854d Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 26 Aug 2022 12:41:47 +0900 Subject: [PATCH 08/37] simplify + async Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/base_estimator_gradient.py | 76 +++++--- .../gradients/base_sampler_gradient.py | 80 +++++--- .../finite_diff_estimator_gradient.py | 94 +++------ .../gradients/finite_diff_sampler_gradient.py | 105 ++++------ .../gradients/lin_comb_estimator_gradient.py | 110 +++++------ .../gradients/lin_comb_sampler_gradient.py | 121 ++++++------ .../param_shift_estimator_gradient.py | 163 ++++++++-------- .../gradients/param_shift_sampler_gradient.py | 182 ++++++++---------- .../gradients/spsa_estimator_gradient.py | 70 ++++--- .../gradients/spsa_sampler_gradient.py | 76 ++++---- qiskit/algorithms/gradients/utils.py | 92 ++------- .../algorithms/test_estimator_gradient.py | 72 +++---- .../algorithms/test_sampler_gradient.py | 45 ++--- 13 files changed, 582 insertions(+), 704 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index e0750b0f1c0d..b1f064e96a4a 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -17,13 +17,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Sequence, Mapping +from collections.abc import Sequence from copy import copy from qiskit.circuit import Parameter, QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.primitives.primitive_job import PrimitiveJob from qiskit.quantum_info.operators.base_operator import BaseOperator from .estimator_gradient_result import EstimatorGradientResult @@ -45,15 +46,9 @@ def __init__( setting. Higher priority setting overrides lower priority setting. """ self._estimator: BaseEstimator = estimator - self._circuits: Sequence[QuantumCircuit] | None = None - if self._circuits is None: - self._circuits = [] - self._circuit_ids: Mapping[int, int] | None = None - if self._circuit_ids is None: - self._circuit_ids = {} self._default_run_options = run_options - def evaluate( + def run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], @@ -61,7 +56,7 @@ def evaluate( parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - """Run the job of the gradients of expectation values. + """Run the job of the estimator gradient on the given circuits. Args: circuits: The list of quantum circuits to compute the gradients. @@ -79,6 +74,52 @@ def evaluate( The job object of the gradients of the expectation values. The i-th result corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + Raises: + QiskitError: Invalid arguments are given. + """ + # Validate the arguments. + self._validate_arguments(circuits, observables, parameter_values, parameters) + # The priority of run option is as follows: + # run_options in `run` method > gradient's default run_options > primitive's default setting. + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + + job = PrimitiveJob( + self._run, circuits, observables, parameter_values, parameters, **run_opts + ) + job.submit() + return job + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + raise NotImplementedError() + + def _validate_arguments( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator | PauliSumOp], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]] | None = None, + ) -> None: + """Validate the arguments of the `evaluate` method. + + Args: + circuits: The list of quantum circuits to compute the gradients. + observables: The list of observables. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The Sequence of Sequence of Parameters to calculate only the gradients of + the specified parameters. Each Sequence of Parameters corresponds to a circuit in + `circuits`. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. + Raises: QiskitError: Invalid arguments are given. """ @@ -116,20 +157,3 @@ def evaluate( f"not match the number of qubits of the {i}-th observable " f"({observable.num_qubits})." ) - - # The priority of run option is as follows: - # run_options in `run` method > gradient's default run_options > primitive's default setting. - run_opts = copy(self._default_run_options) - run_opts.update(**run_options) - return self._evaluate(circuits, observables, parameter_values, parameters, **run_opts) - - @abstractmethod - def _evaluate( - self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, - **run_options, - ) -> EstimatorGradientResult: - raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 526e9e20b47d..3f0974a96bfc 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -17,12 +17,13 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Sequence, Mapping +from collections.abc import Sequence from copy import copy from qiskit.circuit import QuantumCircuit, Parameter from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler +from qiskit.primitives.primitive_job import PrimitiveJob from .sampler_gradient_result import SamplerGradientResult @@ -38,22 +39,16 @@ def __init__(self, sampler: BaseSampler, **run_options): setting. Higher priority setting overrides lower priority setting. """ self._sampler: BaseSampler = sampler - self._circuits: Sequence[QuantumCircuit] | None = None - if self._circuits is None: - self._circuits = [] - self._circuit_ids: Mapping[int, int] | None = None - if self._circuit_ids is None: - self._circuit_ids = {} self._default_run_options = run_options - def evaluate( + def run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - """Run the job of the gradients of the sampling probability. + """Run the job of the sampler gradient on the given circuits. Args: circuits: The list of quantum circuits to compute the gradients. @@ -67,15 +62,56 @@ def evaluate( setting. Higher priority setting overrides lower priority setting. Returns: - The job object of the gradients of the sampling probability. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th - quasi-probability distribution in the i-th result corresponds to the gradients of the - sampling probability for the j-th parameter in ``circuits[i]``. + Primitive job contains the gradients of the sampling probability. The i-th result + corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + The j-th quasi-probability distribution in the i-th result corresponds to the gradients of + the sampling probability for the j-th parameter in ``circuits[i]``. Raises: QiskitError: Invalid arguments are given. """ - # Validation + + # Validate the arguments. + self._validate_arguments(circuits, parameter_values, parameters) + # The priority of run option is as follows: + # run_options in `run` method > gradient's default run_options > primitive's default run_options. + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + job = PrimitiveJob(self._run, circuits, parameter_values, parameters, **run_opts) + job.submit() + return job + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]] | None = None, + **run_options, + ) -> SamplerGradientResult: + """Compute the sampler gradients on the given circuits.""" + raise NotImplementedError() + + def _validate_arguments( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + ): + """Validate the arguments of the `evaluate` method. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The Sequence of Sequence of Parameters to calculate only the gradients of + the specified parameters. Each Sequence of Parameters corresponds to a circuit in + `circuits`. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. + + Raises: + QiskitError: Invalid arguments are given. + """ + # Validate the arguments. if len(circuits) != len(parameter_values): raise QiskitError( f"The number of circuits ({len(circuits)}) does not match " @@ -94,19 +130,3 @@ def evaluate( f"The number of values ({len(parameter_value)}) does not match " f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." ) - - # The priority of run option is as follows: - # run_options in `run` method > gradient's default run_options > primitive's default run_options. - run_opts = copy(self._default_run_options) - run_opts.update(**run_options) - return self._evaluate(circuits, parameter_values, parameters, **run_opts) - - @abstractmethod - def _evaluate( - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, - **run_options, - ) -> SamplerGradientResult: - raise NotImplementedError() diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 2879480a9976..42439d8c847d 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -25,15 +25,14 @@ from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult -from .utils import make_fin_diff_base_parameter_values class FiniteDiffEstimatorGradient(BaseEstimatorGradient): """ - Gradient of Estimator with Finite difference method. + Compute the gradients of the expectation values by finite difference method. """ - def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_options): + def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-2, **run_options): """ Args: estimator: The estimator used to compute the gradients. @@ -43,12 +42,10 @@ def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_option setting. Higher priority setting overrides lower priority setting. """ self._epsilon = epsilon - self._base_parameter_values_dict = None - if self._base_parameter_values_dict is None: - self._base_parameter_values_dict = {} + self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], @@ -56,70 +53,41 @@ def _evaluate( parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the estimator gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + + jobs, result_indices_all = [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): - index = self._circuit_ids.get(id(circuit)) - if index is not None: - circuit_index = index + # indices of parameters to be differentiated + if parameters_ is None: + indices = list(range(circuit.num_parameters)) else: - # if the given circuit is a new one, make base parameter values for + and - epsilon - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._base_parameter_values_dict[ - circuit_index - ] = make_fin_diff_base_parameter_values(circuit, self._epsilon) - self._circuits.append(circuit) - - circuit_parameters = self._circuits[circuit_index].parameters - base_parameter_values_list = [] - gradient_parameter_values = np.zeros(len(circuit_parameters)) + indices = [circuit.parameters.data.index(p) for p in parameters_] + result_indices_all.append(indices) - # a parameter set for the parameter option - parameters = parameters_ or circuit_parameters - param_set = set(parameters) - - result_index = 0 - result_index_map = {} - # bring the base parameter values for parameters only in the specified parameter set. - for i, param in enumerate(circuit_parameters): - gradient_parameter_values[i] = parameter_values_[i] - if param in param_set: - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][i * 2] - ) - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][i * 2 + 1] - ) - result_index_map[param] = result_index - result_index += 1 - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list - ] - gradient_circuits = [self._circuits[circuit_index]] * len( - gradient_parameter_values_list - ) - observable_list = [observable] * len(gradient_parameter_values_list) + offset = np.identity(circuit.num_parameters)[indices, :] + plus = parameter_values_ + self._epsilon * offset + minus = parameter_values_ - self._epsilon * offset + n = 2 * len(indices) job = self._estimator.run( - gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **run_options ) - results = job.result() + jobs.append(job) - # Combines the results and coefficients to reconstruct the gradient - # for the original circuit parameters - values = np.zeros(len(parameter_values_)) - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - # plus - values[i] += results.values[result_index_map[param] * 2] / (2 * self._epsilon) - # minus - values[i] -= results.values[result_index_map[param] * 2 + 1] / (2 * self._epsilon) + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + n = len(result.values) // 2 # is always a multiple of 2 + gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) + indices = result_indices_all[i] + gradient = np.zeros(circuits[i].num_parameters) + gradient[indices] = gradient_ + gradients.append(gradient) - gradients.append(values) return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index d80daa8d559c..c875082a7c93 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -25,16 +25,15 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult -from .utils import make_fin_diff_base_parameter_values class FiniteDiffSamplerGradient(BaseSamplerGradient): - """Compute the gradients of the sampling probability with the finite difference method.""" + """Compute the gradients of the sampling probability by finite difference method.""" def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-6, + epsilon: float = 1e-2, **run_options, ): """ @@ -47,90 +46,54 @@ def __init__( """ self._epsilon = epsilon - self._base_parameter_values_dict = None - if self._base_parameter_values_dict is None: - self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the sampler gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + + jobs, result_indices_all = [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - index = self._circuit_ids.get(id(circuit)) - if index is not None: - circuit_index = index + # indices of parameters to be differentiated + if parameters_ is None: + indices = list(range(circuit.num_parameters)) else: - # if the given circuit a new one, make base parameter values for + and - epsilon - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._base_parameter_values_dict[ - circuit_index - ] = make_fin_diff_base_parameter_values(circuit, self._epsilon) - self._circuits.append(circuit) - - circuit_parameters = self._circuits[circuit_index].parameters - base_parameter_values_list = [] - gradient_parameter_values = np.zeros(len(circuit_parameters)) - - # a parameter set for the parameter option - parameters = parameters_ or circuit_parameters - param_set = set(parameters) - - result_index = 0 - result_index_map = {} - # bring the base parameter values for parameters only in the specified parameter set. - for i, param in enumerate(circuit_parameters): - gradient_parameter_values[i] = parameter_values_[i] - if param in param_set: - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][i * 2] - ) - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][i * 2 + 1] - ) - result_index_map[param] = result_index - result_index += 1 - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list - ] - gradient_circuits = [self._circuits[circuit_index]] * len( - gradient_parameter_values_list - ) - - job = self._sampler.run( - gradient_circuits, gradient_parameter_values_list, **run_options - ) - results = job.result() - - # Combines the results and coefficients to reconstruct the gradient values - # for the original circuit parameters - dists = [Counter() for _ in range(len(parameter_values_))] - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue + indices = [circuit.parameters.data.index(p) for p in parameters_] + result_indices_all.append(indices) + + offset = np.identity(circuit.num_parameters)[indices, :] + plus = parameter_values_ + self._epsilon * offset + minus = parameter_values_ - self._epsilon * offset + n = 2 * len(indices) + + job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **run_options) + jobs.append(job) + + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + n = len(result.quasi_dists) // 2 + dists = [Counter() for _ in range(circuits[i].num_parameters)] + for j, idx in enumerate(result_indices_all[i]): # plus - dists[i].update( - Counter( - { - k: v / (2 * self._epsilon) - for k, v in results.quasi_dists[result_index_map[param] * 2].items() - } - ) + dists[idx].update( + Counter({k: v / (2 * self._epsilon) for k, v in result.quasi_dists[j].items()}) ) # minus - dists[i].update( + dists[idx].update( Counter( { k: -1 * v / (2 * self._epsilon) - for k, v in results.quasi_dists[result_index_map[param] * 2 + 1].items() + for k, v in result.quasi_dists[j + n].items() } ) ) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 6430e63f2c41..f6fd61e40fd7 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -15,7 +15,6 @@ from __future__ import annotations -from collections import defaultdict from typing import Sequence import numpy as np @@ -47,12 +46,10 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = None - if self._gradient_circuit_data_dict is None: - self._gradient_circuit_data_dict = {} + self._gradient_circuit_data_dict = {} super().__init__(estimator, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], @@ -60,70 +57,65 @@ def _evaluate( parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the estimator gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + jobs, result_indices_all, coeffs_all = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): - index = self._circuit_ids.get(id(circuit)) - if index is not None: - circuit_index = index + # a set of parameters to be differentiated + if parameters_ is None: + param_set = set(circuit.parameters) else: - # if circuit is not passed in the constructor. - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._gradient_circuit_data_dict[circuit_index] = make_lin_comb_gradient_circuit( - circuit - ) - self._circuits.append(circuit) - # Add an observable for the auxiliary qubit - observable_ = observable.expand(Pauli_Z) + param_set = set(parameters_) - gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] - circuit_parameters = self._circuits[circuit_index].parameters - parameter_value_map = {} - - # a parameter set for the parameters option - parameters = parameters_ or self._circuits[circuit_index].parameters - param_set = set(parameters) + observable_ = observable.expand(Pauli_Z) + gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) + if gradient_circuit_data is None: + gradient_circuit_data = make_lin_comb_gradient_circuit(circuit) + self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data - result_index = 0 - result_index_map = defaultdict(list) + # only compute the gradients for parameters in the parameter set gradient_circuits = [] - # gradient circuit indices and result indices - for i, param in enumerate(circuit_parameters): - parameter_value_map[param] = parameter_values_[i] - if not param in param_set: - continue - for grad in gradient_circuit_data[param]: - gradient_circuits.append(grad.gradient_circuit) - result_index_map[param].append(result_index) - result_index += 1 - gradient_parameter_values_list = [ - parameter_values_ for i in range(len(gradient_circuits)) - ] - observable_list = [observable_] * len(gradient_circuits) - + result_indices = [] + coeffs = [] + for i, param in enumerate(circuit.parameters): + if param in param_set: + gradient_circuits.extend( + grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] + ) + result_indices.extend(i for _ in gradient_circuit_data[param]) + for grad_data in gradient_circuit_data[param]: + coeff = grad_data.coeff + # if the parameter is a parameter expression, we need to substitute + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + coeffs.append(bound_coeff) + + n = len(gradient_circuits) job = self._estimator.run( - gradient_circuits, observable_list, gradient_parameter_values_list + gradient_circuits, [observable_] * n, [parameter_values_ for _ in range(n)] ) - results = job.result() - - values = np.zeros(len(circuit_parameters)) - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - for j, grad in enumerate(gradient_circuit_data[param]): - coeff = grad.coeff - result_index = result_index_map[param][j] - # if coeff has parameters, substitute them with the given parameter values - if isinstance(coeff, ParameterExpression): - local_map = {p: parameter_value_map[p] for p in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - values[i] += bound_coeff * results.values[result_index_map[param][j]] + jobs.append(job) + result_indices_all.append(result_indices) + coeffs_all.append(coeffs) + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + values = np.zeros(len(circuits[i].parameters)) + for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): + values[idx] += coeff * grad_ gradients.append(values) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index bd60685a4c3b..d4678b5bd0d8 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -15,7 +15,7 @@ from __future__ import annotations -from collections import Counter, defaultdict +from collections import Counter from typing import Sequence from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit @@ -42,78 +42,77 @@ def __init__(self, sampler: BaseSampler, **run_options): setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = None - if self._gradient_circuit_data_dict is None: - self._gradient_circuit_data_dict = {} + self._gradient_circuit_data_dict = {} super().__init__(sampler, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the sampler gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + jobs, result_indices_all, coeffs_all = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - index = self._circuit_ids.get(id(circuit)) - if index is not None: - circuit_index = index + # a set of parameters to be differentiated + if parameters_ is None: + param_set = set(circuit.parameters) else: - # if circuit is not passed in the constructor. - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._gradient_circuit_data_dict[circuit_index] = make_lin_comb_gradient_circuit( + param_set = set(parameters_) + + gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) + if gradient_circuit_data is None: + gradient_circuit_data = make_lin_comb_gradient_circuit( circuit, add_measurement=True ) - self._circuits.append(circuit) - - gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] - circuit_parameters = self._circuits[circuit_index].parameters - parameter_value_map = {} - - # a parameter set for the parameter option - parameters = parameters_ or self._circuits[circuit_index].parameters - param_set = set(parameters) - - result_index = 0 - result_index_map = defaultdict(list) - gradient_circuit = [] - # gradient circuit indices and result indices - for i, param in enumerate(circuit_parameters): - parameter_value_map[param] = parameter_values_[i] - if not param in param_set: - continue - for grad in gradient_circuit_data[param]: - gradient_circuit.append(grad.gradient_circuit) - result_index_map[param].append(result_index) - result_index += 1 - gradient_parameter_values_list = [parameter_values_] * len(gradient_circuit) - - job = self._sampler.run(gradient_circuit, gradient_parameter_values_list, **run_options) - results = job.result() - - param_set = set(parameters) - dists = [Counter() for _ in range(len(parameter_values_))] - num_bitstrings = 2 ** self._circuits[circuit_index].num_qubits - i = 0 - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - for j, grad in enumerate(gradient_circuit_data[param]): - coeff = grad.coeff - result_index = result_index_map[param][j] - # if coeff has parameters, substitute them with the given parameter values - if isinstance(coeff, ParameterExpression): - local_map = {p: parameter_value_map[p] for p in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - for k, v in results.quasi_dists[result_index].items(): - sign, k2 = divmod(k, num_bitstrings) - dists[i][k2] += (-1) ** sign * bound_coeff * v - + self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data + + # only compute the gradients for parameters in the parameter set + gradient_circuits = [] + result_indices = [] + coeffs = [] + for i, param in enumerate(circuit.parameters): + if param in param_set: + gradient_circuits.extend( + grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] + ) + result_indices.extend(i for _ in gradient_circuit_data[param]) + for grad_data in gradient_circuit_data[param]: + coeff = grad_data.coeff + # if the parameter is a parameter expression, we need to substitute + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + coeffs.append(bound_coeff) + + n = len(gradient_circuits) + job = self._sampler.run(gradient_circuits, [parameter_values_ for _ in range(n)]) + jobs.append(job) + result_indices_all.append(result_indices) + coeffs_all.append(coeffs) + + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + dists = [Counter() for _ in range(circuits[i].num_parameters)] + num_bitstrings = 2 ** circuits[i].num_qubits + for grad_quasi_, idx, coeff in zip( + result.quasi_dists, result_indices_all[i], coeffs_all[i] + ): + for k_, v in grad_quasi_.items(): + sign, k = divmod(k_, num_bitstrings) + dists[idx][k] += (-1) ** sign * coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) + return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 1725de0cf330..8b4fc26aa97f 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -34,7 +34,7 @@ class ParamShiftEstimatorGradient(BaseEstimatorGradient): - """Parameter shift estimator gradient""" + """Compute the gradients of the expectation values by the parameter shift rule""" def __init__(self, estimator: BaseEstimator, **run_options): """ @@ -44,16 +44,11 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = None - if self._gradient_circuit_data_dict is None: - self._gradient_circuit_data_dict = {} - - self._base_parameter_values_dict = None - if self._base_parameter_values_dict is None: - self._base_parameter_values_dict = {} + self._gradient_circuit_data_dict = {} + self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], @@ -61,94 +56,92 @@ def _evaluate( parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the estimator gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + + jobs, result_indices_all, coeffs_all = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): - circ_index = self._circuit_ids.get(id(circuit)) - if circ_index is not None: - circuit_index = circ_index + # a set of parameters to be differentiated + if parameters_ is None: + param_set = set(circuit.parameters) else: - # if the given circuit is a new one, make gradient circuit data and - # base parameter values. - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._gradient_circuit_data_dict[ - circuit_index - ] = make_param_shift_gradient_circuit_data(circuit) - self._base_parameter_values_dict[ - circuit_index - ] = make_param_shift_base_parameter_values( - self._gradient_circuit_data_dict[circuit_index] + param_set = set(parameters_) + + gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) + base_parameter_values_all = self._base_parameter_values_dict.get(id(circuit)) + if gradient_circuit_data is None and base_parameter_values_all is None: + gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) + self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data + base_parameter_values_all = make_param_shift_base_parameter_values( + gradient_circuit_data ) - self._circuits.append(circuit) - - gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] - gradient_parameter_map = gradient_circuit_data.gradient_parameter_map - gradient_parameter_index_map = gradient_circuit_data.gradient_parameter_index_map - circuit_parameters = self._circuits[circuit_index].parameters - parameter_value_map = {} - base_parameter_values_list = [] + self._base_parameter_values_dict[id(circuit)] = base_parameter_values_all + + plus_offsets, minus_offsets = [], [] + gradient_circuit = gradient_circuit_data.gradient_circuit gradient_parameter_values = np.zeros( len(gradient_circuit_data.gradient_circuit.parameters) ) - # a parameter set for the parameters option - parameters = parameters_ or self._circuits[circuit_index].parameters - param_set = set(parameters) - - result_index = 0 - result_index_map = {} - # bring the base parameter values for parameters only in the specified parameter set. - for i, param in enumerate(circuit_parameters): - parameter_value_map[param] = parameter_values_[i] - for g_param in gradient_parameter_map[param]: - g_param_idx = gradient_parameter_index_map[g_param] - gradient_parameter_values[g_param_idx] = parameter_values_[i] - if param in param_set: - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][g_param_idx * 2] - ) - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][g_param_idx * 2 + 1] - ) - result_index_map[g_param] = result_index - result_index += 1 - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list + # only compute the gradients for parameters in the parameter set + result_indices = [] + coeffs = [] + for i, param in enumerate(circuit.parameters): + g_params = gradient_circuit_data.gradient_parameter_map[param] + indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + gradient_parameter_values[indices] = parameter_values_[i] + if param in param_set: + plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) + minus_offsets.extend( + base_parameter_values_all[idx + len(gradient_circuit.parameters)] + for idx in indices + ) + result_indices.extend(i for _ in range(len(indices))) + for g_param in g_params: + coeff = gradient_circuit_data.coeff_map[g_param] + # if coeff has parameters, we need to substitute + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + coeffs.append(bound_coeff) + + # add the base parameter values to the parameter values + gradient_parameter_values_plus = [ + gradient_parameter_values + plus_offset for plus_offset in plus_offsets ] - observable_list = [observable] * len(gradient_parameter_values_list) - gradient_circuits = [gradient_circuit_data.gradient_circuit] * len( - gradient_parameter_values_list - ) + gradient_parameter_values_minus = [ + gradient_parameter_values + minus_offset for minus_offset in minus_offsets + ] + n = 2 * len(gradient_parameter_values_plus) job = self._estimator.run( - gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + [gradient_circuit] * n, + [observable] * n, + gradient_parameter_values_plus + gradient_parameter_values_minus, + **run_options, ) - results = job.result() - - # Combines the results and coefficients to reconstruct the gradient - # for the original circuit parameters - values = np.zeros(len(parameter_values_)) - - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - for g_param in gradient_parameter_map[param]: - g_param_idx = gradient_parameter_index_map[g_param] - coeff = gradient_circuit_data.coeff_map[g_param] / 2 - # if coeff has parameters, substitute them with the given parameter values - if isinstance(coeff, ParameterExpression): - local_map = {p: parameter_value_map[p] for p in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - # plus - values[i] += bound_coeff * results.values[result_index_map[g_param] * 2] - # minus - values[i] -= bound_coeff * results.values[result_index_map[g_param] * 2 + 1] + jobs.append(job) + result_indices_all.append(result_indices) + coeffs_all.append(coeffs) + + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + n = len(result.values) // 2 # is always a multiple of 2 + gradient_ = (result.values[:n] - result.values[n:]) / 2 + values = np.zeros(len(circuits[i].parameters)) + for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): + values[idx] += coeff * grad_ gradients.append(values) + return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 49b476dc6654..1551656c84e0 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -30,7 +30,7 @@ class ParamShiftSamplerGradient(BaseSamplerGradient): - """Compute the gradients of the sampling probability with the parameter shift method.""" + """Compute the gradients of the sampling probability by the parameter shift rule.""" def __init__(self, sampler: BaseSampler, **run_options): """ @@ -40,125 +40,105 @@ def __init__(self, sampler: BaseSampler, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = None - if self._gradient_circuit_data_dict is None: - self._gradient_circuit_data_dict = {} - self._base_parameter_values_dict = None - if self._base_parameter_values_dict is None: - self._base_parameter_values_dict = {} + self._gradient_circuit_data_dict = {} + self._base_parameter_values_dict = {} super().__init__(sampler, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] + """Compute the sampler gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] - gradients = [] + jobs, result_indices_all, coeffs_all = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): - index = self._circuit_ids.get(id(circuit)) - if index is not None: - circuit_index = index + # a set of parameters to be differentiated + if parameters_ is None: + param_set = set(circuit.parameters) else: - # if the given circuit is a new one, make gradient circuit data and - # base parameter values - circuit_index = len(self._circuits) - self._circuit_ids[id(circuit)] = circuit_index - self._gradient_circuit_data_dict[ - circuit_index - ] = make_param_shift_gradient_circuit_data(circuit) - self._base_parameter_values_dict[ - circuit_index - ] = make_param_shift_base_parameter_values( - self._gradient_circuit_data_dict[circuit_index] + param_set = set(parameters_) + + gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) + base_parameter_values_all = self._base_parameter_values_dict.get(id(circuit)) + + if gradient_circuit_data is None and base_parameter_values_all is None: + gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) + self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data + base_parameter_values_all = make_param_shift_base_parameter_values( + gradient_circuit_data ) - self._circuits.append(circuit) - - gradient_circuit_data = self._gradient_circuit_data_dict[circuit_index] - gradient_parameter_map = gradient_circuit_data.gradient_parameter_map - gradient_parameter_index_map = gradient_circuit_data.gradient_parameter_index_map - circuit_parameters = self._circuits[circuit_index].parameters - parameter_value_map = {} - base_parameter_values_list = [] + self._base_parameter_values_dict[id(circuit)] = base_parameter_values_all + + plus_offsets, minus_offsets = [], [] + gradient_circuit = gradient_circuit_data.gradient_circuit gradient_parameter_values = np.zeros( len(gradient_circuit_data.gradient_circuit.parameters) ) - # a parameter set for the parameter option - parameters = parameters_ or self._circuits[circuit_index].parameters - param_set = set(parameters) - - result_index = 0 - result_index_map = {} - # bring the base parameter values for parameters only in the specified parameter set. - for i, param in enumerate(circuit_parameters): - parameter_value_map[param] = parameter_values_[i] - for g_param in gradient_parameter_map[param]: - g_param_idx = gradient_parameter_index_map[g_param] - gradient_parameter_values[g_param_idx] = parameter_values_[i] - if param in param_set: - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][g_param_idx * 2] - ) - base_parameter_values_list.append( - self._base_parameter_values_dict[circuit_index][g_param_idx * 2 + 1] - ) - result_index_map[g_param] = result_index - result_index += 1 - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list + # only compute the gradients for parameters in the parameter set + result_map = [] + coeffs = [] + for i, param in enumerate(circuit.parameters): + g_params = gradient_circuit_data.gradient_parameter_map[param] + indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + gradient_parameter_values[indices] = parameter_values_[i] + if param in param_set: + plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) + minus_offsets.extend( + base_parameter_values_all[idx + len(gradient_circuit.parameters)] + for idx in indices + ) + result_map.extend(i for _ in range(len(indices))) + for g_param in g_params: + coeff = gradient_circuit_data.coeff_map[g_param] + # if coeff has parameters, we need to substitute + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + coeffs.append(bound_coeff / 2) + + # add the base parameter values to the parameter values + gradient_parameter_values_plus = [ + gradient_parameter_values + plus_offset for plus_offset in plus_offsets ] - gradient_circuits = [gradient_circuit_data.gradient_circuit] * len( - gradient_parameter_values_list - ) + gradient_parameter_values_minus = [ + gradient_parameter_values + minus_offset for minus_offset in minus_offsets + ] + n = 2 * len(gradient_parameter_values_plus) job = self._sampler.run( - gradient_circuits, gradient_parameter_values_list, **run_options + [gradient_circuit] * n, + gradient_parameter_values_plus + gradient_parameter_values_minus, + **run_options, ) - results = job.result() - - # Combines the results and coefficients to reconstruct the gradient - # for the original circuit parameters - dists = [Counter() for _ in range(len(parameter_values_))] - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - for g_param in gradient_parameter_map[param]: - g_param_idx = gradient_parameter_index_map[g_param] - coeff = gradient_circuit_data.coeff_map[g_param] / 2 - # if coeff has parameters, substitute them with the given parameter values - if isinstance(coeff, ParameterExpression): - local_map = {p: parameter_value_map[p] for p in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - # plus - dists[i].update( - Counter( - { - k: bound_coeff * v - for k, v in results.quasi_dists[ - result_index_map[g_param] * 2 - ].items() - } - ) - ) - # minus - dists[i].update( - Counter( - { - k: -1 * bound_coeff * v - for k, v in results.quasi_dists[ - result_index_map[g_param] * 2 + 1 - ].items() - } - ) - ) + jobs.append(job) + result_indices_all.append(result_map) + coeffs_all.append(coeffs) + + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + n = len(result.quasi_dists) // 2 + dists = [Counter() for _ in range(circuits[i].num_parameters)] + for j, (idx, coeff) in enumerate(zip(result_indices_all[i], coeffs_all[i])): + # plus + dists[idx].update(Counter({k: v * coeff for k, v in result.quasi_dists[j].items()})) + # minus + dists[idx].update( + Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) + ) gradients.append([QuasiDistribution(dist) for dist in dists]) return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 98a2925c5def..c09e5fd38035 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -26,18 +26,18 @@ from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult -from .utils import make_spsa_base_parameter_values class SPSAEstimatorGradient(BaseEstimatorGradient): """ - Gradient of Estimator with the Simultaneous Perturbation Stochastic Approximation (SPSA). + Compute the gradients of the expectation value by the Simultaneous Perturbation Stochastic + Approximation (SPSA). """ def __init__( self, estimator: BaseEstimator, - epsilon: float = 1e-6, + epsilon: float = 1e-2, seed: int | None = None, **run_options, ): @@ -54,7 +54,7 @@ def __init__( super().__init__(estimator, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], @@ -62,42 +62,40 @@ def _evaluate( parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> EstimatorGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] + """Compute the estimator gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] + + jobs, result_indices_all, offsets = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): + # indices of parameters to be differentiated + if parameters_ is None: + indices = list(range(circuit.num_parameters)) + else: + indices = [circuit.parameters.data.index(p) for p in parameters_] + result_indices_all.append(indices) - base_parameter_values_list = make_spsa_base_parameter_values(circuit, self._epsilon) - circuit_parameters = circuit.parameters - gradient_parameter_values = np.zeros(len(circuit_parameters)) - - # a parameter set for the parameter option - parameters = parameters_ or circuit_parameters - param_set = set(parameters) - - gradient_parameter_values = np.array(parameter_values_) - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list - ] - gradient_circuits = [circuit] * len(gradient_parameter_values_list) - observable_list = [observable] * len(gradient_parameter_values_list) - job = self._estimator.run( - gradient_circuits, observable_list, gradient_parameter_values_list, **run_options + offset = np.array( + [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] ) - results = job.result() - # Combines the results and coefficients to reconstruct the gradient - # for the original circuit parameters - values = np.zeros(len(parameter_values_)) - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue - # plus - values[i] += results.values[0] / (2 * base_parameter_values_list[0][i]) - # minus - values[i] -= results.values[1] / (2 * base_parameter_values_list[0][i]) + plus = parameter_values_ + self._epsilon * offset + minus = parameter_values_ - self._epsilon * offset + offsets.append(offset) + + job = self._estimator.run([circuit] * 2, [observable] * 2, [plus, minus], **run_options) + jobs.append(job) - gradients.append(values) + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + n = len(result.values) // 2 # is always a multiple of 2 + gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) + indices = result_indices_all[i] + gradient = np.zeros(circuits[i].num_parameters) + gradient[indices] = gradient_[indices] + gradients.append(gradient) return EstimatorGradientResult(values=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index cfd153a027b5..39c0a5d28823 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -26,18 +26,18 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult -from .utils import make_spsa_base_parameter_values class SPSASamplerGradient(BaseSamplerGradient): """ - Gradient of Sampler with the Simultaneous Perturbation Stochastic Approximation (SPSA). + Compute the gradients of the sampling probability by the Simultaneous Perturbation Stochastic + Approximation (SPSA). """ def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-6, + epsilon: float = 1e-2, seed: int | None = None, **run_options, ): @@ -54,61 +54,61 @@ def __init__( super().__init__(sampler, **run_options) - def _evaluate( + def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, ) -> SamplerGradientResult: - parameters = parameters or [None for _ in range(len(circuits))] - gradients = [] - for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + """Compute the sampler gradients on the given circuits.""" + # if parameters is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] - base_parameter_values_list = make_spsa_base_parameter_values(circuit, self._epsilon) - circuit_parameters = circuit.parameters - gradient_parameter_values = np.zeros(len(circuit_parameters)) - - # a parameter set for the parameter option - parameters = parameters_ or circuit_parameters - param_set = set(parameters) - - gradient_parameter_values = np.array(parameter_values_) - # add the given parameter values and the base parameter values - gradient_parameter_values_list = [ - gradient_parameter_values + base_parameter_values - for base_parameter_values in base_parameter_values_list - ] - gradient_circuits = [circuit] * len(gradient_parameter_values_list) - job = self._sampler.run( - gradient_circuits, gradient_parameter_values_list, **run_options + jobs, result_indices_all, offsets = [], [], [] + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + # indices of parameters to be differentiated + if parameters_ is None: + indices = list(range(circuit.num_parameters)) + else: + indices = [circuit.parameters.data.index(p) for p in parameters_] + result_indices_all.append(indices) + + offset = np.array( + [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] ) - results = job.result() - # Combines the results and coefficients to reconstruct the gradient values - # for the original circuit parameters - dists = [Counter() for _ in range(len(parameter_values_))] - for i, param in enumerate(circuit_parameters): - if param not in param_set: - continue + plus = parameter_values_ + self._epsilon * offset + minus = parameter_values_ - self._epsilon * offset + offsets.append(offset) + + job = self._sampler.run([circuit] * 2, [plus, minus], **run_options) + jobs.append(job) + + # combine the results + results = [job.result() for job in jobs] + gradients = [] + for i, result in enumerate(results): + dists = [Counter() for _ in range(circuits[i].num_parameters)] + for idx in result_indices_all[i]: # plus - dists[i].update( + dists[idx].update( Counter( { - k: v / (2 * base_parameter_values_list[0][i]) - for k, v in results.quasi_dists[0].items() + k: v / (2 * self._epsilon * offsets[i][idx]) + for k, v in result.quasi_dists[0].items() } ) ) # minus - dists[i].update( + dists[idx].update( Counter( { - k: -1 * v / (2 * base_parameter_values_list[0][i]) - for k, v in results.quasi_dists[1].items() + k: -1 * v / (2 * self._epsilon * offsets[i][idx]) + for k, v in result.quasi_dists[1].items() } ) ) - gradients.append([QuasiDistribution(dist) for dist in dists]) return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 181c6a76cb24..3c9ad5a9ed24 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -21,7 +21,6 @@ from collections import defaultdict from copy import deepcopy from dataclasses import dataclass -import random from typing import Dict, List import numpy as np @@ -59,8 +58,6 @@ class ParameterShiftGradientCircuitData: gradient_circuit (QuantumCircuit): An internal quantum circuit used to calculate the gradient gradient_parameter_map (dict): A dictionary maps the parameters of ``circuit`` to the parameters of ``gradient_circuit``. - gradient_parameter_index_map (dict): A dictionary maps the parameters of ``gradient_circuit`` - to their index. gradient_virtual_parameter_map (dict): A dictionary maps the parameters of ``gradient_circuit`` to the virtual parameter variables. A virtual parameter variable is added if a parameter expression has more than one parameter. @@ -71,7 +68,6 @@ class ParameterShiftGradientCircuitData: circuit: QuantumCircuit gradient_circuit: QuantumCircuit gradient_parameter_map: Dict[Parameter, Parameter] - gradient_parameter_index_map: Dict[Parameter, int] gradient_virtual_parameter_map: Dict[Parameter, Parameter] coeff_map: Dict[Parameter, float | ParameterExpression] @@ -168,10 +164,6 @@ def make_param_shift_gradient_circuit_data( new_parameter_variable = Parameter(f"g{parameter_variable.name}_1") subs_map[parameter_variable] = new_parameter_variable g_circuit.global_phase = g_circuit.global_phase.subs(subs_map) - g_parameter_index_map = {} - - for i, g_param in enumerate(g_circuit.parameters): - g_parameter_index_map[g_param] = i return ParameterShiftGradientCircuitData( circuit=circuit2, @@ -179,7 +171,6 @@ def make_param_shift_gradient_circuit_data( gradient_virtual_parameter_map=g_virtual_parameter_map, gradient_parameter_map=g_parameter_map, coeff_map=coeff_map, - gradient_parameter_index_map=g_parameter_index_map, ) @@ -196,50 +187,21 @@ def make_param_shift_base_parameter_values( The base parameter values for the parameter shift method. """ # Make internal parameter values for the parameter shift - num_g_parameters = len(gradient_circuit_data.gradient_circuit.parameters) - base_parameter_values = [] - # Make base decomposed parameter values for each original parameter - for param in gradient_circuit_data.circuit.parameters: - for g_param in gradient_circuit_data.gradient_parameter_map[param]: - # use the related virtual parameter if it exists - if g_param in gradient_circuit_data.gradient_virtual_parameter_map: - g_param = gradient_circuit_data.gradient_virtual_parameter_map[g_param] - g_param_idx = gradient_circuit_data.gradient_parameter_index_map[g_param] - # for + pi/2 in the parameter shift rule - parameter_values_plus = np.zeros(num_g_parameters) - parameter_values_plus[g_param_idx] += np.pi / 2 - base_parameter_values.append(parameter_values_plus) - # for - pi/2 in the parameter shift rule - parameter_values_minus = np.zeros(num_g_parameters) - parameter_values_minus[g_param_idx] -= np.pi / 2 - base_parameter_values.append(parameter_values_minus) - return base_parameter_values - - -def make_fin_diff_base_parameter_values( - circuit: QuantumCircuit, epsilon: float = 1e-6 -) -> List[np.ndarray]: - """Makes base parameter values for the finite difference method. Each base parameter value will - be added to the given parameter values in later calculations. - - Args: - circuit: circuit for the base parameter values. - epsilon: The offset size for the finite difference gradients. - - Returns: - List: The base parameter values for the finite difference method. - """ - base_parameter_values = [] + g_parameters = gradient_circuit_data.gradient_circuit.parameters + plus_offsets = [] + minus_offsets = [] # Make base decomposed parameter values for each original parameter - for i, _ in enumerate(circuit.parameters): - parameter_values_plus = np.zeros(len(circuit.parameters)) - parameter_values_plus[i] += epsilon - base_parameter_values.append(parameter_values_plus) - # for - epsilon in the finite diff - parameter_values_minus = np.zeros(len(circuit.parameters)) - parameter_values_minus[i] -= epsilon - base_parameter_values.append(parameter_values_minus) - return base_parameter_values + for g_param in g_parameters: + if g_param in gradient_circuit_data.gradient_virtual_parameter_map: + g_param = gradient_circuit_data.gradient_virtual_parameter_map[g_param] + idx = g_parameters.data.index(g_param) + plus = np.zeros(len(g_parameters)) + plus[idx] += np.pi / 2 + minus = np.zeros(len(g_parameters)) + minus[idx] -= np.pi / 2 + plus_offsets.append(plus) + minus_offsets.append(minus) + return plus_offsets + minus_offsets @dataclass @@ -361,29 +323,3 @@ def _gate_gradient(gate: Gate) -> Instruction: czx = czx_circ.to_instruction() return czx raise TypeError(f"Unrecognized parameterized gate, {gate}") - - -def make_spsa_base_parameter_values( - circuit: QuantumCircuit, epsilon: float = 1e-6 -) -> List[np.ndarray]: - """Makes base parameter values for the SPSA. Each base parameter value will - be added to the given parameter values in later calculations. - - Args: - circuit: circuit for the base parameter values. - epsilon: The offset size for the finite difference gradients. - - Returns: - List: The base parameter values for the SPSA. - """ - - base_parameter_values = [] - # Make a perturbation vector - parameter_values_plus = np.array( - [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] - ) - parameter_values_plus = epsilon * parameter_values_plus - parameter_values_minus = -parameter_values_plus - base_parameter_values.append(parameter_values_plus) - base_parameter_values.append(parameter_values_minus) - return base_parameter_values diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index db0399134c8a..1733bab92069 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -54,7 +54,7 @@ def test_gradient_p(self, grad): param_list = [[np.pi / 4], [0], [np.pi / 2]] correct_results = [[-1 / np.sqrt(2)], [0], [-1]] for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] + values = gradient.run([qc], [op], [param]).result().values[0] for j, value in enumerate(values): self.assertAlmostEqual(value, correct_results[i][j], 3) @@ -77,7 +77,7 @@ def test_gradient_u(self, grad): param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] correct_results = [[-0.70710678, 0.0, 0.0], [-0.35355339, -0.85355339, -0.85355339]] for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] + values = gradient.run([qc], [op], [param]).result().values[0] for j, value in enumerate(values): self.assertAlmostEqual(value, correct_results[i][j], 3) @@ -108,8 +108,8 @@ def test_gradient_efficient_su2(self, grad): [0, 0, 0, 1, 0, 0, 0, 0], ] for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -128,8 +128,8 @@ def test_gradient_rxx(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -148,8 +148,8 @@ def test_gradient_ryy(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -170,8 +170,8 @@ def test_gradient_rzz(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -190,8 +190,8 @@ def test_gradient_rzx(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -213,8 +213,8 @@ def test_gradient_parameter_coefficient(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -234,8 +234,8 @@ def test_gradient_parameters(self, grad): ] op = SparsePauliOp.from_list([("Z", 1)]) for i, param in enumerate(param_list): - values = gradient.evaluate([qc], [op], [param], parameters=[[a]]).values[0] - np.testing.assert_almost_equal(values, correct_results[i]) + values = gradient.run([qc], [op], [param], parameters=[[a]]).result().values[0] + np.testing.assert_almost_equal(values, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -256,8 +256,8 @@ def test_gradient_multi_arguments(self, grad): [-1], ] op = SparsePauliOp.from_list([("Z", 1)]) - values = gradient.evaluate([qc, qc2], [op] * 2, param_list).values - np.testing.assert_almost_equal(values, correct_results) + values = gradient.run([qc, qc2], [op] * 2, param_list).result().values + np.testing.assert_almost_equal(values, correct_results, 3) c = Parameter("c") qc3 = QuantumCircuit(1) @@ -269,12 +269,14 @@ def test_gradient_multi_arguments(self, grad): [-0.5 if p == c else 0 for p in qc3.parameters], [-0.5, -0.5], ] - values2 = gradient.evaluate( - [qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None] - ).values - np.testing.assert_almost_equal(values2[0], correct_results2[0]) - np.testing.assert_almost_equal(values2[1], correct_results2[1]) - np.testing.assert_almost_equal(values2[2], correct_results2[2]) + values2 = ( + gradient.run([qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None]) + .result() + .values + ) + np.testing.assert_almost_equal(values2[0], correct_results2[0], 3) + np.testing.assert_almost_equal(values2[1], correct_results2[1], 3) + np.testing.assert_almost_equal(values2[2], correct_results2[2], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -289,13 +291,13 @@ def test_gradient_validation(self, grad): param_list = [[np.pi / 4], [np.pi / 2]] op = SparsePauliOp.from_list([("Z", 1)]) with self.assertRaises(QiskitError): - gradient.evaluate([qc], [op], param_list) + gradient.run([qc], [op], param_list) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], [op, op], param_list, parameters=[[a]]) + gradient.run([qc, qc], [op, op], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], [op], param_list, parameters=[[a]]) + gradient.run([qc, qc], [op], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc], [op], [[np.pi / 4, np.pi / 4]]) + gradient.run([qc], [op], [[np.pi / 4, np.pi / 4]]) def test_spsa_gradient(self): """Test the SPSA estimator gradient""" @@ -309,17 +311,19 @@ def test_spsa_gradient(self): correct_results = [[-0.84147098, 0.84147098]] op = SparsePauliOp.from_list([("ZI", 1)]) gradient = SPSAEstimatorGradient(estimator, seed=123) - values = gradient.evaluate([qc], [op], param_list).values - np.testing.assert_almost_equal(values, correct_results) + values = gradient.run([qc], [op], param_list).result().values + np.testing.assert_almost_equal(values, correct_results, 3) # multi parameters gradient = SPSAEstimatorGradient(estimator, seed=123) param_list2 = [[1, 1], [1, 1], [3, 3]] - values2 = gradient.evaluate( - [qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None] - ).values + values2 = ( + gradient.run([qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None]) + .result() + .values + ) correct_results2 = [[-0.84147098, 0.84147098], [0.0, 0.84147098], [-0.14112001, 0.14112001]] - np.testing.assert_almost_equal(values2, correct_results2) + np.testing.assert_almost_equal(values2, correct_results2, 3) if __name__ == "__main__": diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 872bd6d19d03..ce17a2bdf164 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -55,7 +55,7 @@ def test_gradient_p(self, grad): [{0: -0.499999, 1: 0.499999}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -79,7 +79,7 @@ def test_gradient_u(self, grad): [{0: -0.176777, 1: 0.176777}, {0: -0.426777, 1: 0.426777}, {0: -0.426777, 1: 0.426777}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -173,7 +173,7 @@ def test_gradient_efficient_su2(self, grad): ], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -193,7 +193,7 @@ def test_gradient_rxx(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -213,7 +213,7 @@ def test_gradient_ryy(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -235,7 +235,7 @@ def test_gradient_rzz(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -255,7 +255,7 @@ def test_gradient_rzx(self, grad): [{0: -0.5, 1: 0, 2: 0.5, 3: 0}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -329,10 +329,10 @@ def test_gradient_parameter_coefficient(self, grad): ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: - self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 2) @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) def test_gradient_parameters(self, grad): @@ -350,7 +350,7 @@ def test_gradient_parameters(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param], parameters=[[a]]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param], parameters=[[a]]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): if correct_results[i][j]: for k in quasi_dist: @@ -376,7 +376,7 @@ def test_gradient_multi_arguments(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], [{0: -0.499999, 1: 0.499999}], ] - quasi_dists = gradient.evaluate([qc, qc2], param_list).quasi_dists + quasi_dists = gradient.run([qc, qc2], param_list).result().quasi_dists for j, q_dists in enumerate(quasi_dists): quasi_dist = q_dists[0] for k in quasi_dist: @@ -388,9 +388,11 @@ def test_gradient_multi_arguments(self, grad): qc3.ry(a, 0) qc3.measure_all() param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] - quasi_dists = gradient.evaluate( - [qc, qc3, qc3], param_list2, parameters=[[a], [c], None] - ).quasi_dists + quasi_dists = ( + gradient.run([qc, qc3, qc3], param_list2, parameters=[[a], [c], None]) + .result() + .quasi_dists + ) correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], [{0: -0.25, 1: 0.25} if p == c else {} for p in qc3.parameters], @@ -415,11 +417,11 @@ def test_gradient_validation(self, grad): gradient = grad(sampler) param_list = [[np.pi / 4], [np.pi / 2]] with self.assertRaises(QiskitError): - gradient.evaluate([qc], param_list) + gradient.run([qc], param_list) with self.assertRaises(QiskitError): - gradient.evaluate([qc, qc], param_list, parameters=[[a]]) + gradient.run([qc, qc], param_list, parameters=[[a]]) with self.assertRaises(QiskitError): - gradient.evaluate([qc], [[np.pi / 4, np.pi / 4]]) + gradient.run([qc], [[np.pi / 4, np.pi / 4]]) def test_spsa_gradient(self): """Test the SPSA sampler gradient""" @@ -438,9 +440,8 @@ def test_spsa_gradient(self): ], ] gradient = SPSASamplerGradient(sampler, seed=123) - # quasi_dists = gradient.evaluate([qc], param_list).quasi_dists for i, param in enumerate(param_list): - quasi_dists = gradient.evaluate([qc], [param]).quasi_dists[0] + quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] for j, quasi_dist in enumerate(quasi_dists): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -461,9 +462,9 @@ def test_spsa_gradient(self): ], ] gradient = SPSASamplerGradient(sampler, seed=123) - quasi_dists = gradient.evaluate( - [qc] * 3, param_list2, parameters=[None, [b], None] - ).quasi_dists + quasi_dists = ( + gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().quasi_dists + ) for i, result in enumerate(quasi_dists): for j, q_dists in enumerate(result): From 315483841c9b348fcbe5db4aee9ff61ff5d61198 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 26 Aug 2022 15:23:59 +0900 Subject: [PATCH 09/37] added gradient variance Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/finite_diff_estimator_gradient.py | 9 +++++++-- .../gradients/finite_diff_sampler_gradient.py | 7 +++++-- .../gradients/lin_comb_estimator_gradient.py | 14 +++++++++----- .../gradients/lin_comb_sampler_gradient.py | 8 +++++--- .../gradients/param_shift_estimator_gradient.py | 8 ++++++-- .../gradients/param_shift_sampler_gradient.py | 8 +++++--- .../gradients/spsa_estimator_gradient.py | 8 ++++++-- .../algorithms/gradients/spsa_sampler_gradient.py | 9 ++++++--- 8 files changed, 49 insertions(+), 22 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 42439d8c847d..6a7ed5e0d28f 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -14,6 +14,7 @@ from __future__ import annotations +from copy import copy from typing import Sequence import numpy as np @@ -81,13 +82,17 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) indices = result_indices_all[i] gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_ gradients.append(gradient) + d['gradient_variance'] = np.var(gradient_) + metadata_.append(d) - return EstimatorGradientResult(values=gradients, metadata=run_options) + + return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index c875082a7c93..4d3bb66502bc 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -14,6 +14,7 @@ from __future__ import annotations +from copy import copy from collections import Counter from typing import Sequence @@ -79,8 +80,9 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) n = len(result.quasi_dists) // 2 dists = [Counter() for _ in range(circuits[i].num_parameters)] for j, idx in enumerate(result_indices_all[i]): @@ -99,4 +101,5 @@ def _run( ) gradients.append([QuasiDistribution(dist) for dist in dists]) - return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) + metadata_.append(d) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index f6fd61e40fd7..ec10fabaad6c 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -15,6 +15,7 @@ from __future__ import annotations +from copy import copy from typing import Sequence import numpy as np @@ -111,11 +112,14 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): - values = np.zeros(len(circuits[i].parameters)) + d = copy(run_options) + gradient_ = np.zeros(len(circuits[i].parameters)) for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): - values[idx] += coeff * grad_ - gradients.append(values) + gradient_[idx] += coeff * grad_ + gradients.append(gradient_) + d['gradient_variance'] = np.var(gradient_) + metadata_.append(result.metadata) - return EstimatorGradientResult(values=gradients, metadata=run_options) + return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index d4678b5bd0d8..970304907953 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -16,6 +16,7 @@ from __future__ import annotations from collections import Counter +from copy import copy from typing import Sequence from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit @@ -103,8 +104,9 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) dists = [Counter() for _ in range(circuits[i].num_parameters)] num_bitstrings = 2 ** circuits[i].num_qubits for grad_quasi_, idx, coeff in zip( @@ -114,5 +116,5 @@ def _run( sign, k = divmod(k_, num_bitstrings) dists[idx][k] += (-1) ** sign * coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) - - return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) + metadata_.append(d) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 8b4fc26aa97f..94e1214dbec1 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -15,6 +15,7 @@ from __future__ import annotations +from copy import copy from typing import Sequence import numpy as np @@ -135,13 +136,16 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / 2 values = np.zeros(len(circuits[i].parameters)) for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ gradients.append(values) + d['gradient_variance'] = np.var(gradient_) + metadata_.append(result.metadata) - return EstimatorGradientResult(values=gradients, metadata=run_options) + return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 1551656c84e0..f3f60716a733 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -15,6 +15,7 @@ from __future__ import annotations +from copy import copy from collections import Counter from typing import Sequence @@ -128,8 +129,9 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) n = len(result.quasi_dists) // 2 dists = [Counter() for _ in range(circuits[i].num_parameters)] for j, (idx, coeff) in enumerate(zip(result_indices_all[i], coeffs_all[i])): @@ -140,5 +142,5 @@ def _run( Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) ) gradients.append([QuasiDistribution(dist) for dist in dists]) - - return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) + metadata_.append(d) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index c09e5fd38035..8c97527beffa 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -14,6 +14,7 @@ from __future__ import annotations +from copy import copy from typing import Sequence import random @@ -90,12 +91,15 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) indices = result_indices_all[i] gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_[indices] gradients.append(gradient) - return EstimatorGradientResult(values=gradients, metadata=run_options) + d['gradient_variance'] = np.var(gradient_) + metadata_.append(result.metadata) + return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 39c0a5d28823..09d35620d44a 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -14,9 +14,10 @@ from __future__ import annotations +import random from collections import Counter +from copy import copy from typing import Sequence -import random import numpy as np @@ -87,8 +88,9 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients = [] + gradients, metadata_ = [], [] for i, result in enumerate(results): + d = copy(run_options) dists = [Counter() for _ in range(circuits[i].num_parameters)] for idx in result_indices_all[i]: # plus @@ -110,5 +112,6 @@ def _run( ) ) gradients.append([QuasiDistribution(dist) for dist in dists]) + metadata_.append(d) - return SamplerGradientResult(quasi_dists=gradients, metadata=run_options) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) From 44cc926a4b913567a7eb02dd218bbab34ef2b762 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 26 Aug 2022 15:42:53 +0900 Subject: [PATCH 10/37] lint Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/finite_diff_estimator_gradient.py | 3 +-- qiskit/algorithms/gradients/lin_comb_estimator_gradient.py | 2 +- qiskit/algorithms/gradients/param_shift_estimator_gradient.py | 2 +- qiskit/algorithms/gradients/spsa_estimator_gradient.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 6a7ed5e0d28f..aeb95e360a1f 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -91,8 +91,7 @@ def _run( gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_ gradients.append(gradient) - d['gradient_variance'] = np.var(gradient_) + d["gradient_variance"] = np.var(gradient_) metadata_.append(d) - return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index ec10fabaad6c..62b0953727b9 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -119,7 +119,7 @@ def _run( for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): gradient_[idx] += coeff * grad_ gradients.append(gradient_) - d['gradient_variance'] = np.var(gradient_) + d["gradient_variance"] = np.var(gradient_) metadata_.append(result.metadata) return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 94e1214dbec1..30f8012da4f0 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -145,7 +145,7 @@ def _run( for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ gradients.append(values) - d['gradient_variance'] = np.var(gradient_) + d["gradient_variance"] = np.var(gradient_) metadata_.append(result.metadata) return EstimatorGradientResult(values=gradients, metadata=metadata_) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 8c97527beffa..7bf254b39167 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -100,6 +100,6 @@ def _run( gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_[indices] gradients.append(gradient) - d['gradient_variance'] = np.var(gradient_) + d["gradient_variance"] = np.var(gradient_) metadata_.append(result.metadata) return EstimatorGradientResult(values=gradients, metadata=metadata_) From 89353ea24506c7b5c03bbb850b76a104768cfb67 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 26 Aug 2022 16:04:51 +0900 Subject: [PATCH 11/37] added the run_options field Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/estimator_gradient_result.py | 1 + .../algorithms/gradients/finite_diff_estimator_gradient.py | 6 ++---- .../algorithms/gradients/finite_diff_sampler_gradient.py | 6 ++---- qiskit/algorithms/gradients/lin_comb_estimator_gradient.py | 6 ++---- qiskit/algorithms/gradients/lin_comb_sampler_gradient.py | 4 +--- .../algorithms/gradients/param_shift_estimator_gradient.py | 6 ++---- .../algorithms/gradients/param_shift_sampler_gradient.py | 4 +--- qiskit/algorithms/gradients/sampler_gradient_result.py | 1 + qiskit/algorithms/gradients/spsa_estimator_gradient.py | 7 +++---- qiskit/algorithms/gradients/spsa_sampler_gradient.py | 4 +--- 10 files changed, 16 insertions(+), 29 deletions(-) diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index 59aaea523041..757999449545 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -32,3 +32,4 @@ class EstimatorGradientResult: values: list[np.ndarray] metadata: list[dict[str, Any]] + run_options: dict[str, Any] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index aeb95e360a1f..297513cdcb71 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -84,14 +84,12 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) indices = result_indices_all[i] gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_ gradients.append(gradient) - d["gradient_variance"] = np.var(gradient_) - metadata_.append(d) + metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_) + return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 4d3bb66502bc..fd58c33d655e 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -82,7 +82,6 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) n = len(result.quasi_dists) // 2 dists = [Counter() for _ in range(circuits[i].num_parameters)] for j, idx in enumerate(result_indices_all[i]): @@ -99,7 +98,6 @@ def _run( } ) ) - gradients.append([QuasiDistribution(dist) for dist in dists]) - metadata_.append(d) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) + + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 62b0953727b9..f416e81efbe5 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -114,12 +114,10 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) gradient_ = np.zeros(len(circuits[i].parameters)) for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): gradient_[idx] += coeff * grad_ gradients.append(gradient_) - d["gradient_variance"] = np.var(gradient_) - metadata_.append(result.metadata) + metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_) + return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 970304907953..1540c4166a57 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -106,7 +106,6 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) dists = [Counter() for _ in range(circuits[i].num_parameters)] num_bitstrings = 2 ** circuits[i].num_qubits for grad_quasi_, idx, coeff in zip( @@ -116,5 +115,4 @@ def _run( sign, k = divmod(k_, num_bitstrings) dists[idx][k] += (-1) ** sign * coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) - metadata_.append(d) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 30f8012da4f0..a5e53dcad3da 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -138,14 +138,12 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / 2 values = np.zeros(len(circuits[i].parameters)) for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ gradients.append(values) - d["gradient_variance"] = np.var(gradient_) - metadata_.append(result.metadata) + metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_) + return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index f3f60716a733..94a5b9aba28b 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -131,7 +131,6 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) n = len(result.quasi_dists) // 2 dists = [Counter() for _ in range(circuits[i].num_parameters)] for j, (idx, coeff) in enumerate(zip(result_indices_all[i], coeffs_all[i])): @@ -142,5 +141,4 @@ def _run( Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) ) gradients.append([QuasiDistribution(dist) for dist in dists]) - metadata_.append(d) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index d11286f9fca0..84e74e512fe5 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -32,3 +32,4 @@ class SamplerGradientResult: quasi_dists: list[list[QuasiDistribution]] metadata: list[dict[str, Any]] + run_options: dict[str, Any] diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 7bf254b39167..9436d1c3e166 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -93,13 +93,12 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) indices = result_indices_all[i] gradient = np.zeros(circuits[i].num_parameters) gradient[indices] = gradient_[indices] gradients.append(gradient) - d["gradient_variance"] = np.var(gradient_) - metadata_.append(result.metadata) - return EstimatorGradientResult(values=gradients, metadata=metadata_) + metadata_.append({"gradient_variance": np.var(gradient_)}) + + return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 09d35620d44a..b90812c19d2e 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -90,7 +90,6 @@ def _run( results = [job.result() for job in jobs] gradients, metadata_ = [], [] for i, result in enumerate(results): - d = copy(run_options) dists = [Counter() for _ in range(circuits[i].num_parameters)] for idx in result_indices_all[i]: # plus @@ -112,6 +111,5 @@ def _run( ) ) gradients.append([QuasiDistribution(dist) for dist in dists]) - metadata_.append(d) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_) + return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) From 9abbd2ac79ef1177308c236c0be344d2333384c6 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 26 Aug 2022 16:09:15 +0900 Subject: [PATCH 12/37] fix lint Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../algorithms/gradients/finite_diff_estimator_gradient.py | 5 +++-- qiskit/algorithms/gradients/finite_diff_sampler_gradient.py | 5 +++-- qiskit/algorithms/gradients/lin_comb_estimator_gradient.py | 5 +++-- qiskit/algorithms/gradients/lin_comb_sampler_gradient.py | 5 +++-- .../algorithms/gradients/param_shift_estimator_gradient.py | 5 +++-- qiskit/algorithms/gradients/param_shift_sampler_gradient.py | 5 +++-- qiskit/algorithms/gradients/spsa_estimator_gradient.py | 5 +++-- qiskit/algorithms/gradients/spsa_sampler_gradient.py | 5 +++-- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 297513cdcb71..afd27c761b43 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -14,7 +14,6 @@ from __future__ import annotations -from copy import copy from typing import Sequence import numpy as np @@ -92,4 +91,6 @@ def _run( gradients.append(gradient) metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) + return EstimatorGradientResult( + values=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index fd58c33d655e..5dbd04f08569 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -14,7 +14,6 @@ from __future__ import annotations -from copy import copy from collections import Counter from typing import Sequence @@ -100,4 +99,6 @@ def _run( ) gradients.append([QuasiDistribution(dist) for dist in dists]) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) + return SamplerGradientResult( + quasi_dists=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index f416e81efbe5..8e0e47100b9a 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -15,7 +15,6 @@ from __future__ import annotations -from copy import copy from typing import Sequence import numpy as np @@ -120,4 +119,6 @@ def _run( gradients.append(gradient_) metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) + return EstimatorGradientResult( + values=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 1540c4166a57..719129edeb45 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -16,7 +16,6 @@ from __future__ import annotations from collections import Counter -from copy import copy from typing import Sequence from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit @@ -115,4 +114,6 @@ def _run( sign, k = divmod(k_, num_bitstrings) dists[idx][k] += (-1) ** sign * coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) + return SamplerGradientResult( + quasi_dists=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index a5e53dcad3da..e85d1450b2d6 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -15,7 +15,6 @@ from __future__ import annotations -from copy import copy from typing import Sequence import numpy as np @@ -146,4 +145,6 @@ def _run( gradients.append(values) metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) + return EstimatorGradientResult( + values=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 94a5b9aba28b..7c9e34c5ebf3 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -15,7 +15,6 @@ from __future__ import annotations -from copy import copy from collections import Counter from typing import Sequence @@ -141,4 +140,6 @@ def _run( Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) ) gradients.append([QuasiDistribution(dist) for dist in dists]) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) + return SamplerGradientResult( + quasi_dists=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 9436d1c3e166..7449dd7b2c2a 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -14,7 +14,6 @@ from __future__ import annotations -from copy import copy from typing import Sequence import random @@ -101,4 +100,6 @@ def _run( gradients.append(gradient) metadata_.append({"gradient_variance": np.var(gradient_)}) - return EstimatorGradientResult(values=gradients, metadata=metadata_, run_options=run_options) + return EstimatorGradientResult( + values=gradients, metadata=metadata_, run_options=run_options + ) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index b90812c19d2e..09e0249b944c 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -16,7 +16,6 @@ import random from collections import Counter -from copy import copy from typing import Sequence import numpy as np @@ -112,4 +111,6 @@ def _run( ) gradients.append([QuasiDistribution(dist) for dist in dists]) - return SamplerGradientResult(quasi_dists=gradients, metadata=metadata_, run_options=run_options) + return SamplerGradientResult( + quasi_dists=gradients, metadata=metadata_, run_options=run_options + ) From 3cd1dbe1a2dcee12941f6649b3fddb4f6931807e Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Tue, 30 Aug 2022 16:54:03 +0900 Subject: [PATCH 13/37] fix based on comments Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/__init__.py | 11 ++ qiskit/algorithms/gradients/__init__.py | 15 +- .../gradients/base_estimator_gradient.py | 34 ++-- .../gradients/base_sampler_gradient.py | 24 ++- .../gradients/estimator_gradient_result.py | 6 +- .../finite_diff_estimator_gradient.py | 11 +- .../gradients/finite_diff_sampler_gradient.py | 5 +- .../gradients/lin_comb_estimator_gradient.py | 18 +- .../gradients/lin_comb_sampler_gradient.py | 19 +- .../param_shift_estimator_gradient.py | 129 +++++++------ .../gradients/param_shift_sampler_gradient.py | 182 ++++++++++++------ .../gradients/sampler_gradient_result.py | 6 +- .../gradients/spsa_estimator_gradient.py | 16 +- .../gradients/spsa_sampler_gradient.py | 18 +- qiskit/algorithms/gradients/utils.py | 85 +++++++- .../algorithms/test_estimator_gradient.py | 69 ++++--- .../algorithms/test_sampler_gradient.py | 63 +++--- 17 files changed, 437 insertions(+), 274 deletions(-) diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index 30d71a59e59c..9f7993dfce1d 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -140,6 +140,17 @@ ShorResult +Gradients +---------- + +Algorithms to calculate the gradient of a quantum circuit. + +.. autosummary:: + :toctree: ../stubs/ + + gradients + + Linear Solvers -------------- diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py index 6b34a73704eb..99584a80171a 100644 --- a/qiskit/algorithms/gradients/__init__.py +++ b/qiskit/algorithms/gradients/__init__.py @@ -26,19 +26,26 @@ BaseSamplerGradient BaseEstimatorGradient -Gradients +Estimator Gradients ========= .. autosummary:: :toctree: ../stubs/ FiniteDiffEstimatorGradient - FiniteDiffSamplerGradient LinCombEstimatorGradient - LinCombSamplerGradient ParamShiftEstimatorGradient - ParamShiftSamplerGradient SPSAEstimatorGradient + +Sampler Gradients +========= + +.. autosummary:: + :toctree: ../stubs/ + + FiniteDiffSamplerGradient + LinCombSamplerGradient + ParamShiftSamplerGradient SPSASamplerGradient Results diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index b1f064e96a4a..4c0ba0f92dcc 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -21,7 +21,6 @@ from copy import copy from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.primitives.primitive_job import PrimitiveJob @@ -55,7 +54,7 @@ def run( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, - ) -> EstimatorGradientResult: + ) -> PrimitiveJob: """Run the job of the estimator gradient on the given circuits. Args: @@ -72,17 +71,22 @@ def run( Returns: The job object of the gradients of the expectation values. The i-th result corresponds to - ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th + element of the i-th result corresponds to the gradient of the i-th circuit with respect + to the j-th parameter. Raises: QiskitError: Invalid arguments are given. """ + # if ``parameters`` is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] # Validate the arguments. self._validate_arguments(circuits, observables, parameter_values, parameters) # The priority of run option is as follows: - # run_options in `run` method > gradient's default run_options > primitive's default setting. + # run_options in ``run`` method > gradient's default run_options > primitive's default setting. run_opts = copy(self._default_run_options) - run_opts.update(**run_options) + run_opts.update(run_options) job = PrimitiveJob( self._run, circuits, observables, parameter_values, parameters, **run_opts @@ -96,7 +100,7 @@ def _run( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" @@ -107,7 +111,7 @@ def _validate_arguments( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None] | None = None, ) -> None: """Validate the arguments of the `evaluate` method. @@ -121,38 +125,42 @@ def _validate_arguments( each circuit are calculated. Raises: - QiskitError: Invalid arguments are given. + ValueError: Invalid arguments are given. """ # Validation if len(circuits) != len(parameter_values): - raise QiskitError( + raise ValueError( f"The number of circuits ({len(circuits)}) does not match " f"the number of parameter value sets ({len(parameter_values)})." ) if len(circuits) != len(observables): - raise QiskitError( + raise ValueError( f"The number of circuits ({len(circuits)}) does not match " f"the number of observables ({len(observables)})." ) if parameters is not None: if len(circuits) != len(parameters): - raise QiskitError( + raise ValueError( f"The number of circuits ({len(circuits)}) does not match " f"the number of the specified parameter sets ({len(parameters)})." ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if not circuit.num_parameters: + raise ValueError( + f"The {i}-th circuit is not parameterised." + ) if len(parameter_value) != circuit.num_parameters: - raise QiskitError( + raise ValueError( f"The number of values ({len(parameter_value)}) does not match " f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." ) for i, (circuit, observable) in enumerate(zip(circuits, observables)): if circuit.num_qubits != observable.num_qubits: - raise QiskitError( + raise ValueError( f"The number of qubits of the {i}-th circuit ({circuit.num_qubits}) does " f"not match the number of qubits of the {i}-th observable " f"({observable.num_qubits})." diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 3f0974a96bfc..ab541c7252ad 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -21,7 +21,6 @@ from copy import copy from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler from qiskit.primitives.primitive_job import PrimitiveJob from .sampler_gradient_result import SamplerGradientResult @@ -47,7 +46,7 @@ def run( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, - ) -> SamplerGradientResult: + ) -> PrimitiveJob: """Run the job of the sampler gradient on the given circuits. Args: @@ -70,13 +69,15 @@ def run( Raises: QiskitError: Invalid arguments are given. """ - + # if ``parameters`` is none, all parameters in each circuit are differentiated. + if parameters is None: + parameters = [None for _ in range(len(circuits))] # Validate the arguments. self._validate_arguments(circuits, parameter_values, parameters) # The priority of run option is as follows: # run_options in `run` method > gradient's default run_options > primitive's default run_options. run_opts = copy(self._default_run_options) - run_opts.update(**run_options) + run_opts.update(run_options) job = PrimitiveJob(self._run, circuits, parameter_values, parameters, **run_opts) job.submit() return job @@ -86,7 +87,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter]] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" @@ -109,24 +110,29 @@ def _validate_arguments( each circuit are calculated. Raises: - QiskitError: Invalid arguments are given. + ValueError: Invalid arguments are given. """ # Validate the arguments. if len(circuits) != len(parameter_values): - raise QiskitError( + raise ValueError( f"The number of circuits ({len(circuits)}) does not match " f"the number of parameter value sets ({len(parameter_values)})." ) if parameters is not None: if len(circuits) != len(parameters): - raise QiskitError( + raise ValueError( f"The number of circuits ({len(circuits)}) does not match " f"the number of the specified parameter sets ({len(parameters)})." ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if not circuit.num_parameters: + raise ValueError( + f"The {i}-th circuit is not parameterised." + ) + if len(parameter_value) != circuit.num_parameters: - raise QiskitError( + raise ValueError( f"The number of values ({len(parameter_value)}) does not match " f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." ) diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index 757999449545..ebfedf54b3f3 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -26,10 +26,12 @@ class EstimatorGradientResult: """Result of EstimatorGradient. Args: - values: The gradients of the expectation values. + gradients: The gradients of the expectation values. metadata: Additional information about the job. + run_options: run_options for the estimator. Currently, estimator's default run_options is not + included. """ - values: list[np.ndarray] + gradients: list[np.ndarray] metadata: list[dict[str, Any]] run_options: dict[str, Any] diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index afd27c761b43..96496dba00e7 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -32,7 +32,7 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient): Compute the gradients of the expectation values by finite difference method. """ - def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-2, **run_options): + def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_options): """ Args: estimator: The estimator used to compute the gradients. @@ -50,14 +50,10 @@ def _run( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all = [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters @@ -91,6 +87,7 @@ def _run( gradients.append(gradient) metadata_.append({"gradient_variance": np.var(gradient_)}) + # TODO: include primitive's run_options as well return EstimatorGradientResult( - values=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 5dbd04f08569..094acfe5134d 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -33,7 +33,7 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient): def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-2, + epsilon: float = 1e-6, **run_options, ): """ @@ -99,6 +99,7 @@ def _run( ) gradients.append([QuasiDistribution(dist) for dist in dists]) + # TODO: include primitive's run_options as well return SamplerGradientResult( - quasi_dists=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 8e0e47100b9a..c335f5e9e581 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -34,8 +34,11 @@ class LinCombEstimatorGradient(BaseEstimatorGradient): """Compute the gradients of the expectation values. - This method employs a linear combination of unitaries, - see e.g. https://arxiv.org/pdf/1811.11184.pdf + This method employs a linear combination of unitaries [1]. + + **Reference:** + [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 + `arXiv:1811.11184 `_ """ def __init__(self, estimator: BaseEstimator, **run_options): @@ -54,14 +57,10 @@ def _run( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, coeffs_all = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters @@ -71,7 +70,7 @@ def _run( param_set = set(circuit.parameters) else: param_set = set(parameters_) - + # TODO: support measurement in different basis (Y and Z+iY) observable_ = observable.expand(Pauli_Z) gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) if gradient_circuit_data is None: @@ -119,6 +118,7 @@ def _run( gradients.append(gradient_) metadata_.append({"gradient_variance": np.var(gradient_)}) + # TODO: include primitive's run_options as well return EstimatorGradientResult( - values=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 719129edeb45..252716705ea0 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -29,8 +29,11 @@ class LinCombSamplerGradient(BaseSamplerGradient): """Compute the gradients of the sampling probability. - This method employs a linear combination of unitaries, - see e.g. https://arxiv.org/pdf/1811.11184.pdf + This method employs a linear combination of unitaries [1]. + + **Reference:** + [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 + `arXiv:1811.11184 `_ """ def __init__(self, sampler: BaseSampler, **run_options): @@ -49,14 +52,10 @@ def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, coeffs_all = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # a set of parameters to be differentiated @@ -64,7 +63,7 @@ def _run( param_set = set(circuit.parameters) else: param_set = set(parameters_) - + # TODO: support measurement in different basis (Y and Z+iY) gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) if gradient_circuit_data is None: gradient_circuit_data = make_lin_comb_gradient_circuit( @@ -114,6 +113,8 @@ def _run( sign, k = divmod(k_, num_bitstrings) dists[idx][k] += (-1) ** sign * coeff * v gradients.append([QuasiDistribution(dist) for dist in dists]) + + # TODO: include primitive's run_options as well return SamplerGradientResult( - quasi_dists=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index e85d1450b2d6..f09b960dc236 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -27,10 +27,7 @@ from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult -from .utils import ( - make_param_shift_gradient_circuit_data, - make_param_shift_base_parameter_values, -) +from .utils import param_shift_preprocessing, make_param_shift_parameter_values class ParamShiftEstimatorGradient(BaseEstimatorGradient): @@ -44,8 +41,7 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} - self._base_parameter_values_dict = {} + self._gradient_circuits = {} super().__init__(estimator, **run_options) def _run( @@ -53,14 +49,10 @@ def _run( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, coeffs_all = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters @@ -71,60 +63,74 @@ def _run( else: param_set = set(parameters_) - gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) - base_parameter_values_all = self._base_parameter_values_dict.get(id(circuit)) - if gradient_circuit_data is None and base_parameter_values_all is None: - gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) - self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data - base_parameter_values_all = make_param_shift_base_parameter_values( - gradient_circuit_data + if self._gradient_circuits.get(id(circuit)): + gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ + id(circuit) + ] + else: + gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( + circuit + ) + self._gradient_circuits[id(circuit)] = ( + gradient_circuit_data, + base_parameter_values_all, ) - self._base_parameter_values_dict[id(circuit)] = base_parameter_values_all - plus_offsets, minus_offsets = [], [] - gradient_circuit = gradient_circuit_data.gradient_circuit - gradient_parameter_values = np.zeros( - len(gradient_circuit_data.gradient_circuit.parameters) + # plus_offsets, minus_offsets = [], [] + # gradient_circuit = gradient_circuit_data.gradient_circuit + # gradient_parameter_values = np.zeros( + # len(gradient_circuit_data.gradient_circuit.parameters) + # ) + + # # only compute the gradients for parameters in the parameter set + # result_indices = [] + # coeffs = [] + # for i, param in enumerate(circuit.parameters): + # g_params = gradient_circuit_data.gradient_parameter_map[param] + # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + # gradient_parameter_values[indices] = parameter_values_[i] + # if param in param_set: + # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) + # minus_offsets.extend( + # base_parameter_values_all[idx + len(gradient_circuit.parameters)] + # for idx in indices + # ) + # result_indices.extend(i for _ in range(len(indices))) + # for g_param in g_params: + # coeff = gradient_circuit_data.coeff_map[g_param] + # # if coeff has parameters, we need to substitute + # if isinstance(coeff, ParameterExpression): + # local_map = { + # p: parameter_values_[circuit.parameters.data.index(p)] + # for p in coeff.parameters + # } + # bound_coeff = float(coeff.bind(local_map)) + # else: + # # bound_coeff = coeff + # # coeffs.append(bound_coeff / 2) + + # # add the base parameter values to the parameter values + # gradient_parameter_values_plus = [ + # gradient_parameter_values + plus_offset for plus_offset in plus_offsets + # ] + # gradient_parameter_values_minus = [ + # gradient_parameter_values + minus_offset for minus_offset in minus_offsets + # ] + ( + gradient_parameter_values_plus, + gradient_parameter_values_minus, + result_indices, + coeffs, + ) = make_param_shift_parameter_values( + gradient_circuit_data=gradient_circuit_data, + base_parameter_values=base_parameter_values_all, + parameter_values=parameter_values_, + param_set=param_set, ) - - # only compute the gradients for parameters in the parameter set - result_indices = [] - coeffs = [] - for i, param in enumerate(circuit.parameters): - g_params = gradient_circuit_data.gradient_parameter_map[param] - indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] - gradient_parameter_values[indices] = parameter_values_[i] - if param in param_set: - plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) - minus_offsets.extend( - base_parameter_values_all[idx + len(gradient_circuit.parameters)] - for idx in indices - ) - result_indices.extend(i for _ in range(len(indices))) - for g_param in g_params: - coeff = gradient_circuit_data.coeff_map[g_param] - # if coeff has parameters, we need to substitute - if isinstance(coeff, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff.parameters - } - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - coeffs.append(bound_coeff) - - # add the base parameter values to the parameter values - gradient_parameter_values_plus = [ - gradient_parameter_values + plus_offset for plus_offset in plus_offsets - ] - gradient_parameter_values_minus = [ - gradient_parameter_values + minus_offset for minus_offset in minus_offsets - ] n = 2 * len(gradient_parameter_values_plus) job = self._estimator.run( - [gradient_circuit] * n, + [gradient_circuit_data.gradient_circuit] * n, [observable] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, **run_options, @@ -138,13 +144,14 @@ def _run( gradients, metadata_ = [], [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = (result.values[:n] - result.values[n:]) / 2 + gradient_ = result.values[:n] - result.values[n:] values = np.zeros(len(circuits[i].parameters)) for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ gradients.append(values) metadata_.append({"gradient_variance": np.var(gradient_)}) + # TODO: include primitive's run_options as well return EstimatorGradientResult( - values=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 7c9e34c5ebf3..8c5ef4310d79 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -26,7 +26,7 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult -from .utils import make_param_shift_base_parameter_values, make_param_shift_gradient_circuit_data +from .utils import param_shift_preprocessing, make_param_shift_parameter_values class ParamShiftSamplerGradient(BaseSamplerGradient): @@ -40,22 +40,17 @@ def __init__(self, sampler: BaseSampler, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} - self._base_parameter_values_dict = {} + self._gradient_circuits = {} super().__init__(sampler, **run_options) def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, coeffs_all = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # a set of parameters to be differentiated @@ -64,66 +59,133 @@ def _run( else: param_set = set(parameters_) - gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) - base_parameter_values_all = self._base_parameter_values_dict.get(id(circuit)) - - if gradient_circuit_data is None and base_parameter_values_all is None: - gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) - self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data - base_parameter_values_all = make_param_shift_base_parameter_values( - gradient_circuit_data + # if self._gradient_circuits.get(id(circuit)): + # gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ + # id(circuit) + # ] + # else: + # gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( + # circuit + # ) + # self._gradient_circuits[id(circuit)] = ( + # gradient_circuit_data, + # base_parameter_values_all, + # ) + + # plus_offsets, minus_offsets = [], [] + # gradient_circuit = gradient_circuit_data.gradient_circuit + # gradient_parameter_values = np.zeros( + # len(gradient_circuit_data.gradient_circuit.parameters) + # ) + + # # only compute the gradients for parameters in the parameter set + # result_map = [] + # coeffs = [] + # for i, param in enumerate(circuit.parameters): + # g_params = gradient_circuit_data.gradient_parameter_map[param] + # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + # gradient_parameter_values[indices] = parameter_values_[i] + # if param in param_set: + # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) + # minus_offsets.extend( + # base_parameter_values_all[idx + len(gradient_circuit.parameters)] + # for idx in indices + # ) + # result_map.extend(i for _ in range(len(indices))) + # for g_param in g_params: + # coeff = gradient_circuit_data.coeff_map[g_param] + # # if coeff has parameters, we need to substitute + # if isinstance(coeff, ParameterExpression): + # local_map = { + # p: parameter_values_[circuit.parameters.data.index(p)] + # for p in coeff.parameters + # } + # bound_coeff = float(coeff.bind(local_map)) + # else: + # bound_coeff = coeff + # coeffs.append(bound_coeff / 2) + + # # add the base parameter values to the parameter values + # gradient_parameter_values_plus = [ + # gradient_parameter_values + plus_offset for plus_offset in plus_offsets + # ] + # gradient_parameter_values_minus = [ + # gradient_parameter_values + minus_offset for minus_offset in minus_offsets + # ] + + if self._gradient_circuits.get(id(circuit)): + gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ + id(circuit) + ] + else: + gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( + circuit + ) + self._gradient_circuits[id(circuit)] = ( + gradient_circuit_data, + base_parameter_values_all, ) - self._base_parameter_values_dict[id(circuit)] = base_parameter_values_all - plus_offsets, minus_offsets = [], [] - gradient_circuit = gradient_circuit_data.gradient_circuit - gradient_parameter_values = np.zeros( - len(gradient_circuit_data.gradient_circuit.parameters) + # plus_offsets, minus_offsets = [], [] + # gradient_circuit = gradient_circuit_data.gradient_circuit + # gradient_parameter_values = np.zeros( + # len(gradient_circuit_data.gradient_circuit.parameters) + # ) + + # # only compute the gradients for parameters in the parameter set + # result_indices = [] + # coeffs = [] + # for i, param in enumerate(circuit.parameters): + # g_params = gradient_circuit_data.gradient_parameter_map[param] + # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + # gradient_parameter_values[indices] = parameter_values_[i] + # if param in param_set: + # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) + # minus_offsets.extend( + # base_parameter_values_all[idx + len(gradient_circuit.parameters)] + # for idx in indices + # ) + # result_indices.extend(i for _ in range(len(indices))) + # for g_param in g_params: + # coeff = gradient_circuit_data.coeff_map[g_param] + # # if coeff has parameters, we need to substitute + # if isinstance(coeff, ParameterExpression): + # local_map = { + # p: parameter_values_[circuit.parameters.data.index(p)] + # for p in coeff.parameters + # } + # bound_coeff = float(coeff.bind(local_map)) + # else: + # # bound_coeff = coeff + # # coeffs.append(bound_coeff / 2) + + # # add the base parameter values to the parameter values + # gradient_parameter_values_plus = [ + # gradient_parameter_values + plus_offset for plus_offset in plus_offsets + # ] + # gradient_parameter_values_minus = [ + # gradient_parameter_values + minus_offset for minus_offset in minus_offsets + # ] + ( + gradient_parameter_values_plus, + gradient_parameter_values_minus, + result_indices, + coeffs, + ) = make_param_shift_parameter_values( + gradient_circuit_data=gradient_circuit_data, + base_parameter_values=base_parameter_values_all, + parameter_values=parameter_values_, + param_set=param_set, ) - - # only compute the gradients for parameters in the parameter set - result_map = [] - coeffs = [] - for i, param in enumerate(circuit.parameters): - g_params = gradient_circuit_data.gradient_parameter_map[param] - indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] - gradient_parameter_values[indices] = parameter_values_[i] - if param in param_set: - plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) - minus_offsets.extend( - base_parameter_values_all[idx + len(gradient_circuit.parameters)] - for idx in indices - ) - result_map.extend(i for _ in range(len(indices))) - for g_param in g_params: - coeff = gradient_circuit_data.coeff_map[g_param] - # if coeff has parameters, we need to substitute - if isinstance(coeff, ParameterExpression): - local_map = { - p: parameter_values_[circuit.parameters.data.index(p)] - for p in coeff.parameters - } - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - coeffs.append(bound_coeff / 2) - - # add the base parameter values to the parameter values - gradient_parameter_values_plus = [ - gradient_parameter_values + plus_offset for plus_offset in plus_offsets - ] - gradient_parameter_values_minus = [ - gradient_parameter_values + minus_offset for minus_offset in minus_offsets - ] n = 2 * len(gradient_parameter_values_plus) job = self._sampler.run( - [gradient_circuit] * n, + [gradient_circuit_data.gradient_circuit] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, **run_options, ) jobs.append(job) - result_indices_all.append(result_map) + result_indices_all.append(result_indices) coeffs_all.append(coeffs) # combine the results @@ -140,6 +202,8 @@ def _run( Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) ) gradients.append([QuasiDistribution(dist) for dist in dists]) + + # TODO: include primitive's run_options as well return SamplerGradientResult( - quasi_dists=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index 84e74e512fe5..e13214d65855 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -26,10 +26,12 @@ class SamplerGradientResult: """Result of SamplerGradient. Args: - quasi_dists: The gradients of the quasi distributions. + gradients: The gradients of the quasi distributions. metadata: Additional information about the job. + run_options: run_options for the sampler. Currently, sampler's default run_options is not + included. """ - quasi_dists: list[list[QuasiDistribution]] + gradients: list[list[QuasiDistribution]] metadata: list[dict[str, Any]] run_options: dict[str, Any] diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 7449dd7b2c2a..b527a4dbf7e9 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -15,7 +15,6 @@ from __future__ import annotations from typing import Sequence -import random import numpy as np @@ -50,7 +49,7 @@ def __init__( run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting.""" self._epsilon = epsilon - self._seed = random.seed(seed) if seed else None + self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() super().__init__(estimator, **run_options) @@ -59,14 +58,10 @@ def _run( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator | PauliSumOp], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, offsets = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters @@ -78,9 +73,7 @@ def _run( indices = [circuit.parameters.data.index(p) for p in parameters_] result_indices_all.append(indices) - offset = np.array( - [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] - ) + offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) plus = parameter_values_ + self._epsilon * offset minus = parameter_values_ - self._epsilon * offset offsets.append(offset) @@ -100,6 +93,7 @@ def _run( gradients.append(gradient) metadata_.append({"gradient_variance": np.var(gradient_)}) + # TODO: include primitive's run_options as well return EstimatorGradientResult( - values=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 09e0249b944c..b6d04070cef9 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -14,7 +14,6 @@ from __future__ import annotations -import random from collections import Counter from typing import Sequence @@ -50,7 +49,8 @@ def __init__( run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting.""" self._epsilon = epsilon - self._seed = random.seed(seed) if seed else None + self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() + super().__init__(sampler, **run_options) @@ -58,14 +58,10 @@ def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - jobs, result_indices_all, offsets = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # indices of parameters to be differentiated @@ -75,9 +71,8 @@ def _run( indices = [circuit.parameters.data.index(p) for p in parameters_] result_indices_all.append(indices) - offset = np.array( - [(-1) ** (random.randint(0, 1)) for _ in range(len(circuit.parameters))] - ) + offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) + plus = parameter_values_ + self._epsilon * offset minus = parameter_values_ - self._epsilon * offset offsets.append(offset) @@ -111,6 +106,7 @@ def _run( ) gradients.append([QuasiDistribution(dist) for dist in dists]) + # TODO: include primitive's run_options as well return SamplerGradientResult( - quasi_dists=gradients, metadata=metadata_, run_options=run_options + gradients=gradients, metadata=metadata_, run_options=run_options ) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 3c9ad5a9ed24..b89ad7daea38 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -101,6 +101,7 @@ def make_param_shift_gradient_circuit_data( "ryy", "rxx", "rzz", + "rzx", ] circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) @@ -204,6 +205,81 @@ def make_param_shift_base_parameter_values( return plus_offsets + minus_offsets +def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradientCircuitData: + """Preprocessing for the parameter shift method. + + Args: + circuit: The original quantum circuit + + Returns: + necessary data to calculate gradients with the parameter shift method. + """ + gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) + base_parameter_values = make_param_shift_base_parameter_values(gradient_circuit_data) + + return gradient_circuit_data, base_parameter_values + + +def make_param_shift_parameter_values( + gradient_circuit_data: ParameterShiftGradientCircuitData, + base_parameter_values: list[np.ndarray], + parameter_values: np.ndarray, + param_set: set[Parameter], +) -> list[np.ndarray]: + """Makes parameter values for the parameter shift method. Each parameter value will be added to + the base parameter values in later calculations. + + Args: + gradient_circuit_data: gradient circuit data for the parameter shift method. + base_parameter_values: base parameter values for the parameter shift method. + parameter_values: parameter values to be added to the base parameter values. + + Returns: + The parameter values for the parameter shift method. + """ + circuit = gradient_circuit_data.circuit + gradient_circuit = gradient_circuit_data.gradient_circuit + gradient_parameter_values = np.zeros( + len(gradient_circuit_data.gradient_circuit.parameters) + ) + plus_offsets, minus_offsets = [], [] + result_indices = [] + coeffs = [] + for i, param in enumerate(circuit.parameters): + g_params = gradient_circuit_data.gradient_parameter_map[param] + indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] + gradient_parameter_values[indices] = parameter_values[i] + if param in param_set: + plus_offsets.extend(base_parameter_values[idx] for idx in indices) + minus_offsets.extend( + base_parameter_values[idx + len(gradient_circuit.parameters)] + for idx in indices + ) + result_indices.extend(i for _ in range(len(indices))) + for g_param in g_params: + coeff = gradient_circuit_data.coeff_map[g_param] + # if coeff has parameters, we need to substitute + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + coeffs.append(bound_coeff / 2) + + # add the base parameter values to the parameter values + gradient_parameter_values_plus = [ + gradient_parameter_values + plus_offset for plus_offset in plus_offsets + ] + gradient_parameter_values_minus = [ + gradient_parameter_values + minus_offset for minus_offset in minus_offsets + ] + return gradient_parameter_values_plus, gradient_parameter_values_minus, result_indices, coeffs + + + @dataclass class LinearCombGradientCircuit: """Gradient circuit for the linear combination of unitaries method. @@ -253,8 +329,8 @@ def make_lin_comb_gradient_circuit( "y", "z", ] - circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) + circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0) qr_aux = QuantumRegister(1, "aux") cr_aux = ClassicalRegister(1, "aux") circuit2.add_register(qr_aux) @@ -286,37 +362,30 @@ def _gate_gradient(gate: Gate) -> Instruction: """Returns the derivative of the gate""" # pylint: disable=too-many-return-statements if isinstance(gate, RXGate): - # theta return CXGate() if isinstance(gate, RYGate): - # theta return CYGate() if isinstance(gate, RZGate): - # theta return CZGate() if isinstance(gate, RXXGate): - # theta cxx_circ = QuantumCircuit(3) cxx_circ.cx(0, 1) cxx_circ.cx(0, 2) cxx = cxx_circ.to_instruction() return cxx if isinstance(gate, RYYGate): - # theta cyy_circ = QuantumCircuit(3) cyy_circ.cy(0, 1) cyy_circ.cy(0, 2) cyy = cyy_circ.to_instruction() return cyy if isinstance(gate, RZZGate): - # theta czz_circ = QuantumCircuit(3) czz_circ.cz(0, 1) czz_circ.cz(0, 2) czz = czz_circ.to_instruction() return czz if isinstance(gate, RZXGate): - # theta czx_circ = QuantumCircuit(3) czx_circ.cx(0, 2) czx_circ.cz(0, 1) diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 1733bab92069..dfb767de76e3 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -28,7 +28,6 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.exceptions import QiskitError from qiskit.primitives import Estimator from qiskit.quantum_info import SparsePauliOp from qiskit.test import QiskitTestCase @@ -54,8 +53,8 @@ def test_gradient_p(self, grad): param_list = [[np.pi / 4], [0], [np.pi / 2]] correct_results = [[-1 / np.sqrt(2)], [0], [-1]] for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - for j, value in enumerate(values): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + for j, value in enumerate(gradients): self.assertAlmostEqual(value, correct_results[i][j], 3) @combine( @@ -77,8 +76,8 @@ def test_gradient_u(self, grad): param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] correct_results = [[-0.70710678, 0.0, 0.0], [-0.35355339, -0.85355339, -0.85355339]] for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - for j, value in enumerate(values): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + for j, value in enumerate(gradients): self.assertAlmostEqual(value, correct_results[i][j], 3) @combine( @@ -108,8 +107,8 @@ def test_gradient_efficient_su2(self, grad): [0, 0, 0, 1, 0, 0, 0, 0], ] for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -128,8 +127,8 @@ def test_gradient_rxx(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -148,8 +147,8 @@ def test_gradient_ryy(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -170,8 +169,8 @@ def test_gradient_rzz(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -190,8 +189,8 @@ def test_gradient_rzx(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -213,8 +212,8 @@ def test_gradient_parameter_coefficient(self, grad): ] op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -234,8 +233,8 @@ def test_gradient_parameters(self, grad): ] op = SparsePauliOp.from_list([("Z", 1)]) for i, param in enumerate(param_list): - values = gradient.run([qc], [op], [param], parameters=[[a]]).result().values[0] - np.testing.assert_almost_equal(values, correct_results[i], 3) + gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -256,8 +255,8 @@ def test_gradient_multi_arguments(self, grad): [-1], ] op = SparsePauliOp.from_list([("Z", 1)]) - values = gradient.run([qc, qc2], [op] * 2, param_list).result().values - np.testing.assert_almost_equal(values, correct_results, 3) + gradients = gradient.run([qc, qc2], [op] * 2, param_list).result().gradients + np.testing.assert_almost_equal(gradients, correct_results, 3) c = Parameter("c") qc3 = QuantumCircuit(1) @@ -269,14 +268,14 @@ def test_gradient_multi_arguments(self, grad): [-0.5 if p == c else 0 for p in qc3.parameters], [-0.5, -0.5], ] - values2 = ( + gradients2 = ( gradient.run([qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None]) .result() - .values + .gradients ) - np.testing.assert_almost_equal(values2[0], correct_results2[0], 3) - np.testing.assert_almost_equal(values2[1], correct_results2[1], 3) - np.testing.assert_almost_equal(values2[2], correct_results2[2], 3) + np.testing.assert_almost_equal(gradients2[0], correct_results2[0], 3) + np.testing.assert_almost_equal(gradients2[1], correct_results2[1], 3) + np.testing.assert_almost_equal(gradients2[2], correct_results2[2], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -290,13 +289,13 @@ def test_gradient_validation(self, grad): gradient = grad(estimator) param_list = [[np.pi / 4], [np.pi / 2]] op = SparsePauliOp.from_list([("Z", 1)]) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc], [op], param_list) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc, qc], [op, op], param_list, parameters=[[a]]) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc, qc], [op], param_list, parameters=[[a]]) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc], [op], [[np.pi / 4, np.pi / 4]]) def test_spsa_gradient(self): @@ -311,19 +310,19 @@ def test_spsa_gradient(self): correct_results = [[-0.84147098, 0.84147098]] op = SparsePauliOp.from_list([("ZI", 1)]) gradient = SPSAEstimatorGradient(estimator, seed=123) - values = gradient.run([qc], [op], param_list).result().values - np.testing.assert_almost_equal(values, correct_results, 3) + gradients = gradient.run([qc], [op], param_list).result().gradients + np.testing.assert_almost_equal(gradients, correct_results, 3) # multi parameters gradient = SPSAEstimatorGradient(estimator, seed=123) param_list2 = [[1, 1], [1, 1], [3, 3]] - values2 = ( + gradients2 = ( gradient.run([qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None]) .result() - .values + .gradients ) correct_results2 = [[-0.84147098, 0.84147098], [0.0, 0.84147098], [-0.14112001, 0.14112001]] - np.testing.assert_almost_equal(values2, correct_results2, 3) + np.testing.assert_almost_equal(gradients2, correct_results2, 3) if __name__ == "__main__": diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index ce17a2bdf164..396554820a8d 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -28,7 +28,6 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.exceptions import QiskitError from qiskit.primitives import Sampler from qiskit.test import QiskitTestCase @@ -55,8 +54,8 @@ def test_gradient_p(self, grad): [{0: -0.499999, 1: 0.499999}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -79,8 +78,8 @@ def test_gradient_u(self, grad): [{0: -0.176777, 1: 0.176777}, {0: -0.426777, 1: 0.426777}, {0: -0.426777, 1: 0.426777}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -173,8 +172,8 @@ def test_gradient_efficient_su2(self, grad): ], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -193,8 +192,8 @@ def test_gradient_rxx(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -213,8 +212,8 @@ def test_gradient_ryy(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -235,8 +234,8 @@ def test_gradient_rzz(self, grad): [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -255,8 +254,8 @@ def test_gradient_rzx(self, grad): [{0: -0.5, 1: 0, 2: 0.5, 3: 0}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -329,8 +328,8 @@ def test_gradient_parameter_coefficient(self, grad): ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 2) @@ -350,8 +349,8 @@ def test_gradient_parameters(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], ] for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param], parameters=[[a]]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param], parameters=[[a]]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): if correct_results[i][j]: for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @@ -376,8 +375,8 @@ def test_gradient_multi_arguments(self, grad): [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], [{0: -0.499999, 1: 0.499999}], ] - quasi_dists = gradient.run([qc, qc2], param_list).result().quasi_dists - for j, q_dists in enumerate(quasi_dists): + gradients = gradient.run([qc, qc2], param_list).result().gradients + for j, q_dists in enumerate(gradients): quasi_dist = q_dists[0] for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[j][0][k], 3) @@ -388,17 +387,17 @@ def test_gradient_multi_arguments(self, grad): qc3.ry(a, 0) qc3.measure_all() param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] - quasi_dists = ( + gradients = ( gradient.run([qc, qc3, qc3], param_list2, parameters=[[a], [c], None]) .result() - .quasi_dists + .gradients ) correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], [{0: -0.25, 1: 0.25} if p == c else {} for p in qc3.parameters], [{0: -0.25, 1: 0.25}, {0: -0.25, 1: 0.25}], ] - for i, result in enumerate(quasi_dists): + for i, result in enumerate(gradients): for j, q_dists in enumerate(result): if correct_results[i][j]: for k in q_dists: @@ -416,11 +415,11 @@ def test_gradient_validation(self, grad): qc.measure_all() gradient = grad(sampler) param_list = [[np.pi / 4], [np.pi / 2]] - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc], param_list) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc, qc], param_list, parameters=[[a]]) - with self.assertRaises(QiskitError): + with self.assertRaises(ValueError): gradient.run([qc], [[np.pi / 4, np.pi / 4]]) def test_spsa_gradient(self): @@ -441,8 +440,8 @@ def test_spsa_gradient(self): ] gradient = SPSASamplerGradient(sampler, seed=123) for i, param in enumerate(param_list): - quasi_dists = gradient.run([qc], [param]).result().quasi_dists[0] - for j, quasi_dist in enumerate(quasi_dists): + gradients = gradient.run([qc], [param]).result().gradients[0] + for j, quasi_dist in enumerate(gradients): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) # multi parameters @@ -462,11 +461,11 @@ def test_spsa_gradient(self): ], ] gradient = SPSASamplerGradient(sampler, seed=123) - quasi_dists = ( - gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().quasi_dists + gradients = ( + gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().gradients ) - for i, result in enumerate(quasi_dists): + for i, result in enumerate(gradients): for j, q_dists in enumerate(result): if correct_results2[i][j]: for k in q_dists: From 9f04e7668ac2c10713c2233e9ef5b1f9698d12e1 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 31 Aug 2022 00:44:11 +0900 Subject: [PATCH 14/37] wip fix2 Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/base_estimator_gradient.py | 9 +- .../gradients/base_sampler_gradient.py | 9 +- .../finite_diff_estimator_gradient.py | 14 +-- .../gradients/finite_diff_sampler_gradient.py | 38 ++---- .../gradients/lin_comb_estimator_gradient.py | 19 +-- .../gradients/lin_comb_sampler_gradient.py | 40 +++--- .../param_shift_estimator_gradient.py | 51 +------- .../gradients/param_shift_sampler_gradient.py | 119 +++--------------- .../gradients/spsa_estimator_gradient.py | 17 ++- .../gradients/spsa_sampler_gradient.py | 43 +++---- qiskit/algorithms/gradients/utils.py | 8 +- .../algorithms/test_estimator_gradient.py | 15 ++- .../algorithms/test_sampler_gradient.py | 32 ++--- 13 files changed, 136 insertions(+), 278 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index 4c0ba0f92dcc..ddb20abc1ff8 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -43,7 +43,14 @@ def __init__( run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. + + Raises: + ValueError: If the estimator is not an instance of BaseEstimator. """ + if not isinstance(estimator, BaseEstimator): + raise ValueError( + f"The estimator should be an instance of BaseEstimator, but got {type(estimator)}" + ) self._estimator: BaseEstimator = estimator self._default_run_options = run_options @@ -76,7 +83,7 @@ def run( to the j-th parameter. Raises: - QiskitError: Invalid arguments are given. + ValueError: Invalid arguments are given. """ # if ``parameters`` is none, all parameters in each circuit are differentiated. if parameters is None: diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index ab541c7252ad..138640a74a18 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -36,7 +36,14 @@ def __init__(self, sampler: BaseSampler, **run_options): run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. + + Raises: + ValueError: If the sampler is not an instance of BaseEstimator. """ + if not isinstance(sampler, BaseSampler): + raise ValueError( + f"The sampler should be an instance of BaseSampler, but got {type(sampler)}" + ) self._sampler: BaseSampler = sampler self._default_run_options = run_options @@ -67,7 +74,7 @@ def run( the sampling probability for the j-th parameter in ``circuits[i]``. Raises: - QiskitError: Invalid arguments are given. + ValueError: Invalid arguments are given. """ # if ``parameters`` is none, all parameters in each circuit are differentiated. if parameters is None: diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 96496dba00e7..4f0a691f2323 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -54,7 +54,7 @@ def _run( **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - jobs, result_indices_all = [], [] + jobs, metadata_ = [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): @@ -63,7 +63,7 @@ def _run( indices = list(range(circuit.num_parameters)) else: indices = [circuit.parameters.data.index(p) for p in parameters_] - result_indices_all.append(indices) + metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) offset = np.identity(circuit.num_parameters)[indices, :] plus = parameter_values_ + self._epsilon * offset @@ -77,15 +77,11 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] - for i, result in enumerate(results): + gradients = [] + for result in results: n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) - indices = result_indices_all[i] - gradient = np.zeros(circuits[i].num_parameters) - gradient[indices] = gradient_ - gradients.append(gradient) - metadata_.append({"gradient_variance": np.var(gradient_)}) + gradients.append(gradient_) # TODO: include primitive's run_options as well return EstimatorGradientResult( diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 094acfe5134d..dee42a5a175f 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -52,52 +52,38 @@ def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], - parameters: Sequence[Sequence[Parameter] | None] | None = None, + parameters: Sequence[Sequence[Parameter] | None], **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - # if parameters is none, all parameters in each circuit are differentiated. - if parameters is None: - parameters = [None for _ in range(len(circuits))] - - jobs, result_indices_all = [], [] + jobs, metadata_ = [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # indices of parameters to be differentiated if parameters_ is None: indices = list(range(circuit.num_parameters)) else: indices = [circuit.parameters.data.index(p) for p in parameters_] - result_indices_all.append(indices) - + metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) offset = np.identity(circuit.num_parameters)[indices, :] plus = parameter_values_ + self._epsilon * offset minus = parameter_values_ - self._epsilon * offset n = 2 * len(indices) - job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **run_options) jobs.append(job) # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients= [] for i, result in enumerate(results): n = len(result.quasi_dists) // 2 - dists = [Counter() for _ in range(circuits[i].num_parameters)] - for j, idx in enumerate(result_indices_all[i]): - # plus - dists[idx].update( - Counter({k: v / (2 * self._epsilon) for k, v in result.quasi_dists[j].items()}) - ) - # minus - dists[idx].update( - Counter( - { - k: -1 * v / (2 * self._epsilon) - for k, v in result.quasi_dists[j + n].items() - } - ) - ) - gradients.append([QuasiDistribution(dist) for dist in dists]) + gradient_ = [] + for dist_plus, dist_minus in zip(result.quasi_dists[:n], result.quasi_dists[n:]): + grad_dist = np.zeros(2 ** circuits[i].num_qubits) + grad_dist[list(dist_plus.keys())] += list(dist_plus.values()) + grad_dist[list(dist_minus.keys())] -= list(dist_minus.values()) + grad_dist /= (2 * self._epsilon) + gradient_.append({i:dist for i, dist in enumerate(grad_dist)}) + gradients.append(gradient_) # TODO: include primitive's run_options as well return SamplerGradientResult( diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index c335f5e9e581..a86834489c59 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -61,7 +61,7 @@ def _run( **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - jobs, result_indices_all, coeffs_all = [], [], [] + jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): @@ -70,6 +70,8 @@ def _run( param_set = set(circuit.parameters) else: param_set = set(parameters_) + metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) + # TODO: support measurement in different basis (Y and Z+iY) observable_ = observable.expand(Pauli_Z) gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) @@ -78,15 +80,16 @@ def _run( self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data # only compute the gradients for parameters in the parameter set - gradient_circuits = [] - result_indices = [] - coeffs = [] + gradient_circuits, result_indices, coeffs = [], [], [] + result_idx = 0 for i, param in enumerate(circuit.parameters): if param in param_set: gradient_circuits.extend( grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] ) - result_indices.extend(i for _ in gradient_circuit_data[param]) + + result_indices.extend(result_idx for _ in gradient_circuit_data[param]) + result_idx += 1 for grad_data in gradient_circuit_data[param]: coeff = grad_data.coeff # if the parameter is a parameter expression, we need to substitute @@ -102,7 +105,7 @@ def _run( n = len(gradient_circuits) job = self._estimator.run( - gradient_circuits, [observable_] * n, [parameter_values_ for _ in range(n)] + gradient_circuits, [observable_] * n, [parameter_values_] * n, **run_options ) jobs.append(job) result_indices_all.append(result_indices) @@ -110,9 +113,9 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): - gradient_ = np.zeros(len(circuits[i].parameters)) + gradient_ = np.zeros(len(metadata_[i]['parameters'])) for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): gradient_[idx] += coeff * grad_ gradients.append(gradient_) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 252716705ea0..32afb550a88a 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -18,6 +18,8 @@ from collections import Counter from typing import Sequence +import numpy as np + from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.primitives import BaseSampler from qiskit.result import QuasiDistribution @@ -56,13 +58,15 @@ def _run( **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - jobs, result_indices_all, coeffs_all = [], [], [] + jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # a set of parameters to be differentiated if parameters_ is None: param_set = set(circuit.parameters) else: param_set = set(parameters_) + metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) + # TODO: support measurement in different basis (Y and Z+iY) gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) if gradient_circuit_data is None: @@ -72,15 +76,15 @@ def _run( self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data # only compute the gradients for parameters in the parameter set - gradient_circuits = [] - result_indices = [] - coeffs = [] + gradient_circuits, result_indices, coeffs = [], [], [] + result_idx = 0 for i, param in enumerate(circuit.parameters): if param in param_set: gradient_circuits.extend( grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] ) - result_indices.extend(i for _ in gradient_circuit_data[param]) + result_indices.extend(result_idx for _ in gradient_circuit_data[param]) + result_idx += 1 for grad_data in gradient_circuit_data[param]: coeff = grad_data.coeff # if the parameter is a parameter expression, we need to substitute @@ -95,24 +99,28 @@ def _run( coeffs.append(bound_coeff) n = len(gradient_circuits) - job = self._sampler.run(gradient_circuits, [parameter_values_ for _ in range(n)]) + job = self._sampler.run(gradient_circuits, [parameter_values_]*n, **run_options) jobs.append(job) result_indices_all.append(result_indices) coeffs_all.append(coeffs) # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): - dists = [Counter() for _ in range(circuits[i].num_parameters)] - num_bitstrings = 2 ** circuits[i].num_qubits - for grad_quasi_, idx, coeff in zip( - result.quasi_dists, result_indices_all[i], coeffs_all[i] - ): - for k_, v in grad_quasi_.items(): - sign, k = divmod(k_, num_bitstrings) - dists[idx][k] += (-1) ** sign * coeff * v - gradients.append([QuasiDistribution(dist) for dist in dists]) + n = 2 ** circuits[i].num_qubits + grad_dists = np.zeros((len(metadata_[i]["parameters"]), n)) + for idx, coeff, dist in zip( + result_indices_all[i], coeffs_all[i], result.quasi_dists): + grad_dists[idx][list(dist.keys())[:n]] += (np.array(list(dist.values())[:n]) * coeff) + grad_dists[idx][list(dist.keys())[:n]] -= (np.array(list(dist.values())[n:]) * coeff) + + gradient_ = [] + for grad_dist in grad_dists: + gradient_.append( + {i: dist for i, dist in enumerate(grad_dist)} + ) + gradients.append(gradient_) # TODO: include primitive's run_options as well return SamplerGradientResult( diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index f09b960dc236..d21615d0ed18 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -53,7 +53,7 @@ def _run( **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - jobs, result_indices_all, coeffs_all = [], [], [] + jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): @@ -62,6 +62,7 @@ def _run( param_set = set(circuit.parameters) else: param_set = set(parameters_) + metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ @@ -76,46 +77,6 @@ def _run( base_parameter_values_all, ) - # plus_offsets, minus_offsets = [], [] - # gradient_circuit = gradient_circuit_data.gradient_circuit - # gradient_parameter_values = np.zeros( - # len(gradient_circuit_data.gradient_circuit.parameters) - # ) - - # # only compute the gradients for parameters in the parameter set - # result_indices = [] - # coeffs = [] - # for i, param in enumerate(circuit.parameters): - # g_params = gradient_circuit_data.gradient_parameter_map[param] - # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] - # gradient_parameter_values[indices] = parameter_values_[i] - # if param in param_set: - # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) - # minus_offsets.extend( - # base_parameter_values_all[idx + len(gradient_circuit.parameters)] - # for idx in indices - # ) - # result_indices.extend(i for _ in range(len(indices))) - # for g_param in g_params: - # coeff = gradient_circuit_data.coeff_map[g_param] - # # if coeff has parameters, we need to substitute - # if isinstance(coeff, ParameterExpression): - # local_map = { - # p: parameter_values_[circuit.parameters.data.index(p)] - # for p in coeff.parameters - # } - # bound_coeff = float(coeff.bind(local_map)) - # else: - # # bound_coeff = coeff - # # coeffs.append(bound_coeff / 2) - - # # add the base parameter values to the parameter values - # gradient_parameter_values_plus = [ - # gradient_parameter_values + plus_offset for plus_offset in plus_offsets - # ] - # gradient_parameter_values_minus = [ - # gradient_parameter_values + minus_offset for minus_offset in minus_offsets - # ] ( gradient_parameter_values_plus, gradient_parameter_values_minus, @@ -128,7 +89,6 @@ def _run( param_set=param_set, ) n = 2 * len(gradient_parameter_values_plus) - job = self._estimator.run( [gradient_circuit_data.gradient_circuit] * n, [observable] * n, @@ -141,15 +101,14 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = result.values[:n] - result.values[n:] - values = np.zeros(len(circuits[i].parameters)) + gradient_ = (result.values[:n] - result.values[n:]) + values = np.zeros(len(metadata_[i]["parameters"])) for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ gradients.append(values) - metadata_.append({"gradient_variance": np.var(gradient_)}) # TODO: include primitive's run_options as well return EstimatorGradientResult( diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 8c5ef4310d79..c1c06fc156a6 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -51,67 +51,14 @@ def _run( **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - jobs, result_indices_all, coeffs_all = [], [], [] + jobs, result_indices_all, coeffs_all, metadata_ = [], [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # a set of parameters to be differentiated if parameters_ is None: param_set = set(circuit.parameters) else: param_set = set(parameters_) - - # if self._gradient_circuits.get(id(circuit)): - # gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ - # id(circuit) - # ] - # else: - # gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( - # circuit - # ) - # self._gradient_circuits[id(circuit)] = ( - # gradient_circuit_data, - # base_parameter_values_all, - # ) - - # plus_offsets, minus_offsets = [], [] - # gradient_circuit = gradient_circuit_data.gradient_circuit - # gradient_parameter_values = np.zeros( - # len(gradient_circuit_data.gradient_circuit.parameters) - # ) - - # # only compute the gradients for parameters in the parameter set - # result_map = [] - # coeffs = [] - # for i, param in enumerate(circuit.parameters): - # g_params = gradient_circuit_data.gradient_parameter_map[param] - # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] - # gradient_parameter_values[indices] = parameter_values_[i] - # if param in param_set: - # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) - # minus_offsets.extend( - # base_parameter_values_all[idx + len(gradient_circuit.parameters)] - # for idx in indices - # ) - # result_map.extend(i for _ in range(len(indices))) - # for g_param in g_params: - # coeff = gradient_circuit_data.coeff_map[g_param] - # # if coeff has parameters, we need to substitute - # if isinstance(coeff, ParameterExpression): - # local_map = { - # p: parameter_values_[circuit.parameters.data.index(p)] - # for p in coeff.parameters - # } - # bound_coeff = float(coeff.bind(local_map)) - # else: - # bound_coeff = coeff - # coeffs.append(bound_coeff / 2) - - # # add the base parameter values to the parameter values - # gradient_parameter_values_plus = [ - # gradient_parameter_values + plus_offset for plus_offset in plus_offsets - # ] - # gradient_parameter_values_minus = [ - # gradient_parameter_values + minus_offset for minus_offset in minus_offsets - # ] + metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ @@ -126,46 +73,6 @@ def _run( base_parameter_values_all, ) - # plus_offsets, minus_offsets = [], [] - # gradient_circuit = gradient_circuit_data.gradient_circuit - # gradient_parameter_values = np.zeros( - # len(gradient_circuit_data.gradient_circuit.parameters) - # ) - - # # only compute the gradients for parameters in the parameter set - # result_indices = [] - # coeffs = [] - # for i, param in enumerate(circuit.parameters): - # g_params = gradient_circuit_data.gradient_parameter_map[param] - # indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] - # gradient_parameter_values[indices] = parameter_values_[i] - # if param in param_set: - # plus_offsets.extend(base_parameter_values_all[idx] for idx in indices) - # minus_offsets.extend( - # base_parameter_values_all[idx + len(gradient_circuit.parameters)] - # for idx in indices - # ) - # result_indices.extend(i for _ in range(len(indices))) - # for g_param in g_params: - # coeff = gradient_circuit_data.coeff_map[g_param] - # # if coeff has parameters, we need to substitute - # if isinstance(coeff, ParameterExpression): - # local_map = { - # p: parameter_values_[circuit.parameters.data.index(p)] - # for p in coeff.parameters - # } - # bound_coeff = float(coeff.bind(local_map)) - # else: - # # bound_coeff = coeff - # # coeffs.append(bound_coeff / 2) - - # # add the base parameter values to the parameter values - # gradient_parameter_values_plus = [ - # gradient_parameter_values + plus_offset for plus_offset in plus_offsets - # ] - # gradient_parameter_values_minus = [ - # gradient_parameter_values + minus_offset for minus_offset in minus_offsets - # ] ( gradient_parameter_values_plus, gradient_parameter_values_minus, @@ -190,18 +97,20 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): n = len(result.quasi_dists) // 2 - dists = [Counter() for _ in range(circuits[i].num_parameters)] - for j, (idx, coeff) in enumerate(zip(result_indices_all[i], coeffs_all[i])): - # plus - dists[idx].update(Counter({k: v * coeff for k, v in result.quasi_dists[j].items()})) - # minus - dists[idx].update( - Counter({k: -v * coeff for k, v in result.quasi_dists[j + n].items()}) - ) - gradients.append([QuasiDistribution(dist) for dist in dists]) + grad_dists = np.zeros((len(metadata_[i]["parameters"]), 2 ** circuits[i].num_qubits)) + for idx, coeff, dist_plus, dist_minus in zip( + result_indices_all[i], coeffs_all[i], result.quasi_dists[:n], result.quasi_dists[n:] + ): + grad_dists[idx][list(dist_plus.keys())] += (np.array(list(dist_plus.values())) * coeff) + grad_dists[idx][list(dist_minus.keys())] -= (np.array(list(dist_minus.values())) * coeff) + + gradient_ = [] + for grad_dist in grad_dists: + gradient_.append({i: dist for i, dist in enumerate(grad_dist)}) + gradients.append(gradient_) # TODO: include primitive's run_options as well return SamplerGradientResult( diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index b527a4dbf7e9..b034754f54a7 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -36,7 +36,7 @@ class SPSAEstimatorGradient(BaseEstimatorGradient): def __init__( self, estimator: BaseEstimator, - epsilon: float = 1e-2, + epsilon: float = 1e-6, seed: int | None = None, **run_options, ): @@ -62,7 +62,7 @@ def _run( **run_options, ) -> EstimatorGradientResult: """Compute the estimator gradients on the given circuits.""" - jobs, result_indices_all, offsets = [], [], [] + jobs, offsets, metadata_ = [], [], [] for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): @@ -71,7 +71,7 @@ def _run( indices = list(range(circuit.num_parameters)) else: indices = [circuit.parameters.data.index(p) for p in parameters_] - result_indices_all.append(indices) + metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) plus = parameter_values_ + self._epsilon * offset @@ -83,15 +83,12 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) - indices = result_indices_all[i] - gradient = np.zeros(circuits[i].num_parameters) - gradient[indices] = gradient_[indices] - gradients.append(gradient) - metadata_.append({"gradient_variance": np.var(gradient_)}) + gradient = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) + indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] + gradients.append(gradient[indices]) # TODO: include primitive's run_options as well return EstimatorGradientResult( diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index b6d04070cef9..0795707b52dd 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -36,7 +36,7 @@ class SPSASamplerGradient(BaseSamplerGradient): def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-2, + epsilon: float = 1e-6, seed: int | None = None, **run_options, ): @@ -51,7 +51,6 @@ def __init__( self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() - super().__init__(sampler, **run_options) def _run( @@ -62,14 +61,14 @@ def _run( **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - jobs, result_indices_all, offsets = [], [], [] + jobs, result_indices_all, offsets, metadata_ = [], [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # indices of parameters to be differentiated if parameters_ is None: indices = list(range(circuit.num_parameters)) else: indices = [circuit.parameters.data.index(p) for p in parameters_] - result_indices_all.append(indices) + metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) @@ -82,29 +81,21 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients, metadata_ = [], [] + gradients = [] for i, result in enumerate(results): - dists = [Counter() for _ in range(circuits[i].num_parameters)] - for idx in result_indices_all[i]: - # plus - dists[idx].update( - Counter( - { - k: v / (2 * self._epsilon * offsets[i][idx]) - for k, v in result.quasi_dists[0].items() - } - ) - ) - # minus - dists[idx].update( - Counter( - { - k: -1 * v / (2 * self._epsilon * offsets[i][idx]) - for k, v in result.quasi_dists[1].items() - } - ) - ) - gradients.append([QuasiDistribution(dist) for dist in dists]) + grad_dists = np.zeros(2 ** circuits[i].num_qubits) + dist_plus = result.quasi_dists[0] + dist_minus = result.quasi_dists[1] + grad_dists[list(dist_plus.keys())] += list(dist_plus.values()) + grad_dists[list(dist_minus.keys())] -= list(dist_minus.values()) + grad_dists /= 2 * self._epsilon + + indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] + gradient_ = [] + for j in indices: + gradient_.append({k: offsets[i][j] * dist for k, dist in enumerate(grad_dists)}) + + gradients.append(gradient_) # TODO: include primitive's run_options as well return SamplerGradientResult( diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index b89ad7daea38..827f029083e5 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -242,9 +242,8 @@ def make_param_shift_parameter_values( gradient_parameter_values = np.zeros( len(gradient_circuit_data.gradient_circuit.parameters) ) - plus_offsets, minus_offsets = [], [] - result_indices = [] - coeffs = [] + plus_offsets, minus_offsets, result_indices, coeffs = [], [], [], [] + result_idx = 0 for i, param in enumerate(circuit.parameters): g_params = gradient_circuit_data.gradient_parameter_map[param] indices = [gradient_circuit.parameters.data.index(g_param) for g_param in g_params] @@ -255,7 +254,8 @@ def make_param_shift_parameter_values( base_parameter_values[idx + len(gradient_circuit.parameters)] for idx in indices ) - result_indices.extend(i for _ in range(len(indices))) + result_indices.extend(result_idx for _ in range(len(indices))) + result_idx += 1 for g_param in g_params: coeff = gradient_circuit_data.coeff_map[g_param] # if coeff has parameters, we need to substitute diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index dfb767de76e3..62b9c9db83e0 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -28,7 +28,7 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.primitives import Estimator +from qiskit.primitives import Estimator, Sampler from qiskit.quantum_info import SparsePauliOp from qiskit.test import QiskitTestCase @@ -229,7 +229,7 @@ def test_gradient_parameters(self, grad): gradient = grad(estimator) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ - [-0.70710678 if p == a else 0 for p in qc.parameters], + [-0.70710678], ] op = SparsePauliOp.from_list([("Z", 1)]) for i, param in enumerate(param_list): @@ -265,7 +265,7 @@ def test_gradient_multi_arguments(self, grad): param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] correct_results2 = [ [-0.70710678], - [-0.5 if p == c else 0 for p in qc3.parameters], + [-0.5], [-0.5, -0.5], ] gradients2 = ( @@ -289,6 +289,8 @@ def test_gradient_validation(self, grad): gradient = grad(estimator) param_list = [[np.pi / 4], [np.pi / 2]] op = SparsePauliOp.from_list([("Z", 1)]) + with self.assertRaises(ValueError): + _ = grad(Sampler()) with self.assertRaises(ValueError): gradient.run([qc], [op], param_list) with self.assertRaises(ValueError): @@ -313,7 +315,7 @@ def test_spsa_gradient(self): gradients = gradient.run([qc], [op], param_list).result().gradients np.testing.assert_almost_equal(gradients, correct_results, 3) - # multi parameters + #multi parameters gradient = SPSAEstimatorGradient(estimator, seed=123) param_list2 = [[1, 1], [1, 1], [3, 3]] gradients2 = ( @@ -321,8 +323,9 @@ def test_spsa_gradient(self): .result() .gradients ) - correct_results2 = [[-0.84147098, 0.84147098], [0.0, 0.84147098], [-0.14112001, 0.14112001]] - np.testing.assert_almost_equal(gradients2, correct_results2, 3) + correct_results2 = [[-0.84147098, 0.84147098], [0.84147098], [-0.14112001, 0.14112001]] + for grad, correct in zip(gradients2, correct_results2): + np.testing.assert_almost_equal(grad, correct, 3) if __name__ == "__main__": diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 396554820a8d..7d312cf8b31a 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -28,7 +28,7 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.primitives import Sampler +from qiskit.primitives import Estimator, Sampler from qiskit.test import QiskitTestCase @@ -346,16 +346,13 @@ def test_gradient_parameters(self, grad): gradient = grad(sampler) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {}], + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], ] for i, param in enumerate(param_list): gradients = gradient.run([qc], [param], parameters=[[a]]).result().gradients[0] for j, quasi_dist in enumerate(gradients): - if correct_results[i][j]: - for k in quasi_dist: - self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - else: - self.assertEqual(quasi_dist, {}) + for k in quasi_dist: + self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) def test_gradient_multi_arguments(self, grad): @@ -394,16 +391,13 @@ def test_gradient_multi_arguments(self, grad): ) correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], - [{0: -0.25, 1: 0.25} if p == c else {} for p in qc3.parameters], + [{0: -0.25, 1: 0.25}], [{0: -0.25, 1: 0.25}, {0: -0.25, 1: 0.25}], ] for i, result in enumerate(gradients): for j, q_dists in enumerate(result): - if correct_results[i][j]: - for k in q_dists: - self.assertAlmostEqual(q_dists[k], correct_results[i][j][k], 3) - else: - self.assertEqual(q_dists, {}) + for k in q_dists: + self.assertAlmostEqual(q_dists[k], correct_results[i][j][k], 3) @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) def test_gradient_validation(self, grad): @@ -415,6 +409,8 @@ def test_gradient_validation(self, grad): qc.measure_all() gradient = grad(sampler) param_list = [[np.pi / 4], [np.pi / 2]] + with self.assertRaises(ValueError): + _ = grad(Estimator()) with self.assertRaises(ValueError): gradient.run([qc], param_list) with self.assertRaises(ValueError): @@ -452,8 +448,7 @@ def test_spsa_gradient(self): {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, ], [ - {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111} if p == b else {} - for p in qc.parameters + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, ], [ {0: -0.0141129, 1: -0.0564471, 2: -0.3642884, 3: 0.4348484}, @@ -467,11 +462,8 @@ def test_spsa_gradient(self): for i, result in enumerate(gradients): for j, q_dists in enumerate(result): - if correct_results2[i][j]: - for k in q_dists: - self.assertAlmostEqual(q_dists[k], correct_results2[i][j][k], 3) - else: - self.assertEqual(q_dists, {}) + for k in q_dists: + self.assertAlmostEqual(q_dists[k], correct_results2[i][j][k], 3) if __name__ == "__main__": From a7d183fe3d7d230c38ef11d95cf2eb369c41938a Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 31 Aug 2022 09:52:51 +0900 Subject: [PATCH 15/37] fix Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/finite_diff_sampler_gradient.py | 8 +- .../gradients/lin_comb_estimator_gradient.py | 17 ++- .../gradients/lin_comb_sampler_gradient.py | 33 ++--- .../param_shift_estimator_gradient.py | 12 +- .../gradients/param_shift_sampler_gradient.py | 16 ++- .../gradients/spsa_sampler_gradient.py | 10 +- qiskit/algorithms/gradients/utils.py | 13 +- .../algorithms/test_estimator_gradient.py | 115 +++++------------- .../algorithms/test_sampler_gradient.py | 106 ++++------------ 9 files changed, 107 insertions(+), 223 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index dee42a5a175f..92a33562a4ff 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -14,14 +14,12 @@ from __future__ import annotations -from collections import Counter from typing import Sequence import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -73,7 +71,7 @@ def _run( # combine the results results = [job.result() for job in jobs] - gradients= [] + gradients = [] for i, result in enumerate(results): n = len(result.quasi_dists) // 2 gradient_ = [] @@ -81,8 +79,8 @@ def _run( grad_dist = np.zeros(2 ** circuits[i].num_qubits) grad_dist[list(dist_plus.keys())] += list(dist_plus.values()) grad_dist[list(dist_minus.keys())] -= list(dist_minus.values()) - grad_dist /= (2 * self._epsilon) - gradient_.append({i:dist for i, dist in enumerate(grad_dist)}) + grad_dist /= 2 * self._epsilon + gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) # TODO: include primitive's run_options as well diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index a86834489c59..ab204864ee72 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -49,7 +49,7 @@ def __init__(self, estimator: BaseEstimator, **run_options): run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} + self._gradient_circuits = {} super().__init__(estimator, **run_options) def _run( @@ -74,10 +74,10 @@ def _run( # TODO: support measurement in different basis (Y and Z+iY) observable_ = observable.expand(Pauli_Z) - gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) - if gradient_circuit_data is None: - gradient_circuit_data = make_lin_comb_gradient_circuit(circuit) - self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data + gradient_circuits_ = self._gradient_circuits.get(id(circuit)) + if gradient_circuits_ is None: + gradient_circuits_ = make_lin_comb_gradient_circuit(circuit) + self._gradient_circuits[id(circuit)] = gradient_circuits_ # only compute the gradients for parameters in the parameter set gradient_circuits, result_indices, coeffs = [], [], [] @@ -85,12 +85,12 @@ def _run( for i, param in enumerate(circuit.parameters): if param in param_set: gradient_circuits.extend( - grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] + grad.gradient_circuit for grad in gradient_circuits_[param] ) - result_indices.extend(result_idx for _ in gradient_circuit_data[param]) + result_indices.extend(result_idx for _ in gradient_circuits_[param]) result_idx += 1 - for grad_data in gradient_circuit_data[param]: + for grad_data in gradient_circuits_[param]: coeff = grad_data.coeff # if the parameter is a parameter expression, we need to substitute if isinstance(coeff, ParameterExpression): @@ -119,7 +119,6 @@ def _run( for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): gradient_[idx] += coeff * grad_ gradients.append(gradient_) - metadata_.append({"gradient_variance": np.var(gradient_)}) # TODO: include primitive's run_options as well return EstimatorGradientResult( diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 32afb550a88a..08b0f2839258 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -15,14 +15,12 @@ from __future__ import annotations -from collections import Counter from typing import Sequence import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -47,7 +45,7 @@ def __init__(self, sampler: BaseSampler, **run_options): setting. Higher priority setting overrides lower priority setting. """ - self._gradient_circuit_data_dict = {} + self._gradient_circuits = {} super().__init__(sampler, **run_options) def _run( @@ -68,12 +66,10 @@ def _run( metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) # TODO: support measurement in different basis (Y and Z+iY) - gradient_circuit_data = self._gradient_circuit_data_dict.get(id(circuit)) - if gradient_circuit_data is None: - gradient_circuit_data = make_lin_comb_gradient_circuit( - circuit, add_measurement=True - ) - self._gradient_circuit_data_dict[id(circuit)] = gradient_circuit_data + gradient_circuits_ = self._gradient_circuits.get(id(circuit)) + if gradient_circuits_ is None: + gradient_circuits_ = make_lin_comb_gradient_circuit(circuit, add_measurement=True) + self._gradient_circuits[id(circuit)] = gradient_circuits_ # only compute the gradients for parameters in the parameter set gradient_circuits, result_indices, coeffs = [], [], [] @@ -81,11 +77,11 @@ def _run( for i, param in enumerate(circuit.parameters): if param in param_set: gradient_circuits.extend( - grad_data.gradient_circuit for grad_data in gradient_circuit_data[param] + grad.gradient_circuit for grad in gradient_circuits_[param] ) - result_indices.extend(result_idx for _ in gradient_circuit_data[param]) + result_indices.extend(result_idx for _ in gradient_circuits_[param]) result_idx += 1 - for grad_data in gradient_circuit_data[param]: + for grad_data in gradient_circuits_[param]: coeff = grad_data.coeff # if the parameter is a parameter expression, we need to substitute if isinstance(coeff, ParameterExpression): @@ -99,7 +95,7 @@ def _run( coeffs.append(bound_coeff) n = len(gradient_circuits) - job = self._sampler.run(gradient_circuits, [parameter_values_]*n, **run_options) + job = self._sampler.run(gradient_circuits, [parameter_values_] * n, **run_options) jobs.append(job) result_indices_all.append(result_indices) coeffs_all.append(coeffs) @@ -110,16 +106,13 @@ def _run( for i, result in enumerate(results): n = 2 ** circuits[i].num_qubits grad_dists = np.zeros((len(metadata_[i]["parameters"]), n)) - for idx, coeff, dist in zip( - result_indices_all[i], coeffs_all[i], result.quasi_dists): - grad_dists[idx][list(dist.keys())[:n]] += (np.array(list(dist.values())[:n]) * coeff) - grad_dists[idx][list(dist.keys())[:n]] -= (np.array(list(dist.values())[n:]) * coeff) + for idx, coeff, dist in zip(result_indices_all[i], coeffs_all[i], result.quasi_dists): + grad_dists[idx][list(dist.keys())[:n]] += np.array(list(dist.values())[:n]) * coeff + grad_dists[idx][list(dist.keys())[:n]] -= np.array(list(dist.values())[n:]) * coeff gradient_ = [] for grad_dist in grad_dists: - gradient_.append( - {i: dist for i, dist in enumerate(grad_dist)} - ) + gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) # TODO: include primitive's run_options as well diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index d21615d0ed18..45a03a43aab1 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -19,7 +19,7 @@ import numpy as np -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.circuit import Parameter, QuantumCircuit from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -65,15 +65,15 @@ def _run( metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): - gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ + gradient_circuit, base_parameter_values_all = self._gradient_circuits[ id(circuit) ] else: - gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( + gradient_circuit, base_parameter_values_all = param_shift_preprocessing( circuit ) self._gradient_circuits[id(circuit)] = ( - gradient_circuit_data, + gradient_circuit, base_parameter_values_all, ) @@ -83,14 +83,14 @@ def _run( result_indices, coeffs, ) = make_param_shift_parameter_values( - gradient_circuit_data=gradient_circuit_data, + gradient_circuit_data=gradient_circuit, base_parameter_values=base_parameter_values_all, parameter_values=parameter_values_, param_set=param_set, ) n = 2 * len(gradient_parameter_values_plus) job = self._estimator.run( - [gradient_circuit_data.gradient_circuit] * n, + [gradient_circuit.gradient_circuit] * n, [observable] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, **run_options, diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index c1c06fc156a6..f907fc21831a 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -15,14 +15,12 @@ from __future__ import annotations -from collections import Counter from typing import Sequence import numpy as np -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -61,15 +59,15 @@ def _run( metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): - gradient_circuit_data, base_parameter_values_all = self._gradient_circuits[ + gradient_circuit, base_parameter_values_all = self._gradient_circuits[ id(circuit) ] else: - gradient_circuit_data, base_parameter_values_all = param_shift_preprocessing( + gradient_circuit, base_parameter_values_all = param_shift_preprocessing( circuit ) self._gradient_circuits[id(circuit)] = ( - gradient_circuit_data, + gradient_circuit, base_parameter_values_all, ) @@ -79,7 +77,7 @@ def _run( result_indices, coeffs, ) = make_param_shift_parameter_values( - gradient_circuit_data=gradient_circuit_data, + gradient_circuit_data=gradient_circuit, base_parameter_values=base_parameter_values_all, parameter_values=parameter_values_, param_set=param_set, @@ -87,7 +85,7 @@ def _run( n = 2 * len(gradient_parameter_values_plus) job = self._sampler.run( - [gradient_circuit_data.gradient_circuit] * n, + [gradient_circuit.gradient_circuit] * n, gradient_parameter_values_plus + gradient_parameter_values_minus, **run_options, ) @@ -109,7 +107,7 @@ def _run( gradient_ = [] for grad_dist in grad_dists: - gradient_.append({i: dist for i, dist in enumerate(grad_dist)}) + gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) # TODO: include primitive's run_options as well diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 0795707b52dd..c243f2f5dcd3 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -14,14 +14,12 @@ from __future__ import annotations -from collections import Counter from typing import Sequence import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit.result import QuasiDistribution from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -61,7 +59,7 @@ def _run( **run_options, ) -> SamplerGradientResult: """Compute the sampler gradients on the given circuits.""" - jobs, result_indices_all, offsets, metadata_ = [], [], [], [] + jobs, offsets, metadata_ = [], [], [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # indices of parameters to be differentiated if parameters_ is None: @@ -92,8 +90,10 @@ def _run( indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] gradient_ = [] - for j in indices: - gradient_.append({k: offsets[i][j] * dist for k, dist in enumerate(grad_dists)}) + + gradient_.extend( + {k: offsets[i][j] * dist for k, dist in enumerate(grad_dists)} for j in indices + ) gradients.append(gradient_) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 827f029083e5..164494045eb2 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -50,7 +50,7 @@ @dataclass -class ParameterShiftGradientCircuitData: +class ParameterShiftGradientCircuit: """Stores gradient circuit data for the parameter shift method Args: @@ -74,7 +74,7 @@ class ParameterShiftGradientCircuitData: def make_param_shift_gradient_circuit_data( circuit: QuantumCircuit, -) -> ParameterShiftGradientCircuitData: +) -> ParameterShiftGradientCircuit: """Makes a gradient circuit data for the parameter shift method. This re-assigns each parameter in ``circuit`` to a unique parameter, and construct a new gradient circuit with those new parameters. Also, it makes maps used in later calculations. @@ -166,7 +166,7 @@ def make_param_shift_gradient_circuit_data( subs_map[parameter_variable] = new_parameter_variable g_circuit.global_phase = g_circuit.global_phase.subs(subs_map) - return ParameterShiftGradientCircuitData( + return ParameterShiftGradientCircuit( circuit=circuit2, gradient_circuit=g_circuit, gradient_virtual_parameter_map=g_virtual_parameter_map, @@ -176,7 +176,7 @@ def make_param_shift_gradient_circuit_data( def make_param_shift_base_parameter_values( - gradient_circuit_data: ParameterShiftGradientCircuitData, + gradient_circuit_data: ParameterShiftGradientCircuit, ) -> List[np.ndarray]: """Makes base parameter values for the parameter shift method. Each base parameter value will be added to the given parameter values in later calculations. @@ -205,7 +205,7 @@ def make_param_shift_base_parameter_values( return plus_offsets + minus_offsets -def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradientCircuitData: +def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradientCircuit: """Preprocessing for the parameter shift method. Args: @@ -221,7 +221,7 @@ def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradient def make_param_shift_parameter_values( - gradient_circuit_data: ParameterShiftGradientCircuitData, + gradient_circuit_data: ParameterShiftGradientCircuit, base_parameter_values: list[np.ndarray], parameter_values: np.ndarray, param_set: set[Parameter], @@ -233,6 +233,7 @@ def make_param_shift_parameter_values( gradient_circuit_data: gradient circuit data for the parameter shift method. base_parameter_values: base parameter values for the parameter shift method. parameter_values: parameter values to be added to the base parameter values. + param_set: set of parameters to be used in the parameter shift method. Returns: The parameter values for the parameter shift method. diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 62b9c9db83e0..eed4e39b56f9 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -20,14 +20,16 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import ( - FiniteDiffEstimatorGradient, - ParamShiftEstimatorGradient, - LinCombEstimatorGradient, - SPSAEstimatorGradient, -) +from qiskit.algorithms.gradients import (FiniteDiffEstimatorGradient, + LinCombEstimatorGradient, + ParamShiftEstimatorGradient, + SPSAEstimatorGradient) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.circuit.library.standard_gates.rxx import RXXGate +from qiskit.circuit.library.standard_gates.ryy import RYYGate +from qiskit.circuit.library.standard_gates.rzx import RZXGate +from qiskit.circuit.library.standard_gates.rzz import RZZGate from qiskit.primitives import Estimator, Sampler from qiskit.quantum_info import SparsePauliOp from qiskit.test import QiskitTestCase @@ -111,86 +113,31 @@ def test_gradient_efficient_su2(self, grad): np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) - def test_gradient_rxx(self, grad): - """Test the estimator gradient for rxx""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.rxx(a, 0, 1) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) - - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient], ) - def test_gradient_ryy(self, grad): - """Test the estimator gradient for ryy""" + def test_gradient_2qubit_gate(self, grad): + """Test the estimator gradient for 2 qubit gates""" estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.ryy(a, 0, 1) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + for gate in [RXXGate, RYYGate, RZZGate, RZXGate]: + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + a = Parameter("a") + qc = QuantumCircuit(2) + gradient = grad(estimator) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) - def test_gradient_rzz(self, grad): - """Test the estimator gradient for rzz""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.h([0, 1]) - qc.rzz(a, 0, 1) - qc.h([0, 1]) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) - - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) - def test_gradient_rzx(self, grad): - """Test the estimator gradient for rzx""" - estimator = Estimator() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.rzx(a, 0, 1) - gradient = grad(estimator) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [-0.70710678], - [-1], - ] - op = SparsePauliOp.from_list([("ZI", 1)]) - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + if gate is RZZGate: + qc.h([0, 1]) + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + qc.h([0, 1]) + else: + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_almost_equal(gradients, correct_results[i], 3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -315,7 +262,7 @@ def test_spsa_gradient(self): gradients = gradient.run([qc], [op], param_list).result().gradients np.testing.assert_almost_equal(gradients, correct_results, 3) - #multi parameters + # multi parameters gradient = SPSAEstimatorGradient(estimator, seed=123) param_list2 = [[1, 1], [1, 1], [3, 3]] gradients2 = ( diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 7d312cf8b31a..9fd3ee0cd4aa 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -20,14 +20,16 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import ( - FiniteDiffSamplerGradient, - LinCombSamplerGradient, - ParamShiftSamplerGradient, - SPSASamplerGradient, -) +from qiskit.algorithms.gradients import (FiniteDiffSamplerGradient, + LinCombSamplerGradient, + ParamShiftSamplerGradient, + SPSASamplerGradient) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.circuit.library.standard_gates.rxx import RXXGate +from qiskit.circuit.library.standard_gates.ryy import RYYGate +from qiskit.circuit.library.standard_gates.rzx import RZXGate +from qiskit.circuit.library.standard_gates.rzz import RZZGate from qiskit.primitives import Estimator, Sampler from qiskit.test import QiskitTestCase @@ -178,82 +180,28 @@ def test_gradient_efficient_su2(self, grad): self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) - def test_gradient_rxx(self, grad): - """Test the sampler gradient for rxx""" + def test_gradient_2qubit_gate(self, grad): + """Test the sampler gradient for 2 qubit gates""" sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.rxx(a, 0, 1) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], - [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - for j, quasi_dist in enumerate(gradients): - for k in quasi_dist: - self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) + for gate in [RXXGate, RYYGate, RZZGate, RZXGate]: + param_list = [[np.pi / 4], [np.pi / 2]] - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) - def test_gradient_ryy(self, grad): - """Test the sampler gradient for ryy""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.ryy(a, 0, 1) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], - [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - for j, quasi_dist in enumerate(gradients): - for k in quasi_dist: - self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) - def test_gradient_rzz(self, grad): - """Test the sampler gradient for rzz""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.h([0, 1]) - qc.rzz(a, 0, 1) - qc.h([0, 1]) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], - [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], - ] - for i, param in enumerate(param_list): - gradients = gradient.run([qc], [param]).result().gradients[0] - for j, quasi_dist in enumerate(gradients): - for k in quasi_dist: - self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) - def test_gradient_rzx(self, grad): - """Test the sampler gradient for rzx""" - sampler = Sampler() - a = Parameter("a") - qc = QuantumCircuit(2) - qc.rzx(a, 0, 1) - qc.measure_all() - gradient = grad(sampler) - param_list = [[np.pi / 4], [np.pi / 2]] - correct_results = [ - [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0.5 / np.sqrt(2), 3: 0}], - [{0: -0.5, 1: 0, 2: 0.5, 3: 0}], - ] + if gate is RZXGate: + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0.5 / np.sqrt(2), 3: 0}], + [{0: -0.5, 1: 0, 2: 0.5, 3: 0}], + ] + else: + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], + [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], + ] for i, param in enumerate(param_list): + a = Parameter("a") + qc = QuantumCircuit(2) + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + qc.measure_all() + gradient = grad(sampler) gradients = gradient.run([qc], [param]).result().gradients[0] for j, quasi_dist in enumerate(gradients): for k in quasi_dist: From 0a3f70035346754b8a06306dd491e86728741553 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 31 Aug 2022 09:54:41 +0900 Subject: [PATCH 16/37] lint Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/base_estimator_gradient.py | 8 +++----- .../gradients/base_sampler_gradient.py | 8 +++----- .../gradients/lin_comb_estimator_gradient.py | 2 +- .../gradients/param_shift_estimator_gradient.py | 10 +++------- .../gradients/param_shift_sampler_gradient.py | 16 ++++++++-------- qiskit/algorithms/gradients/utils.py | 8 ++------ .../python/algorithms/test_estimator_gradient.py | 10 ++++++---- test/python/algorithms/test_sampler_gradient.py | 10 ++++++---- 8 files changed, 32 insertions(+), 40 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index ddb20abc1ff8..ec7a182387b5 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -50,7 +50,7 @@ def __init__( if not isinstance(estimator, BaseEstimator): raise ValueError( f"The estimator should be an instance of BaseEstimator, but got {type(estimator)}" - ) + ) self._estimator: BaseEstimator = estimator self._default_run_options = run_options @@ -155,10 +155,8 @@ def _validate_arguments( ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): - if not circuit.num_parameters: - raise ValueError( - f"The {i}-th circuit is not parameterised." - ) + if not circuit.num_parameters: + raise ValueError(f"The {i}-th circuit is not parameterised.") if len(parameter_value) != circuit.num_parameters: raise ValueError( f"The number of values ({len(parameter_value)}) does not match " diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 138640a74a18..61dbdf00b040 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -43,7 +43,7 @@ def __init__(self, sampler: BaseSampler, **run_options): if not isinstance(sampler, BaseSampler): raise ValueError( f"The sampler should be an instance of BaseSampler, but got {type(sampler)}" - ) + ) self._sampler: BaseSampler = sampler self._default_run_options = run_options @@ -133,10 +133,8 @@ def _validate_arguments( ) for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): - if not circuit.num_parameters: - raise ValueError( - f"The {i}-th circuit is not parameterised." - ) + if not circuit.num_parameters: + raise ValueError(f"The {i}-th circuit is not parameterised.") if len(parameter_value) != circuit.num_parameters: raise ValueError( diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index ab204864ee72..750ef97a9730 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -115,7 +115,7 @@ def _run( results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): - gradient_ = np.zeros(len(metadata_[i]['parameters'])) + gradient_ = np.zeros(len(metadata_[i]["parameters"])) for grad_, idx, coeff in zip(result.values, result_indices_all[i], coeffs_all[i]): gradient_[idx] += coeff * grad_ gradients.append(gradient_) diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 45a03a43aab1..0c962d5fcb63 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -65,13 +65,9 @@ def _run( metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): - gradient_circuit, base_parameter_values_all = self._gradient_circuits[ - id(circuit) - ] + gradient_circuit, base_parameter_values_all = self._gradient_circuits[id(circuit)] else: - gradient_circuit, base_parameter_values_all = param_shift_preprocessing( - circuit - ) + gradient_circuit, base_parameter_values_all = param_shift_preprocessing(circuit) self._gradient_circuits[id(circuit)] = ( gradient_circuit, base_parameter_values_all, @@ -104,7 +100,7 @@ def _run( gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = (result.values[:n] - result.values[n:]) + gradient_ = result.values[:n] - result.values[n:] values = np.zeros(len(metadata_[i]["parameters"])) for grad_, idx, coeff in zip(gradient_, result_indices_all[i], coeffs_all[i]): values[idx] += coeff * grad_ diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index f907fc21831a..b5a10a856d70 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -59,13 +59,9 @@ def _run( metadata_.append({"parameters": [p for p in circuit.parameters if p in param_set]}) if self._gradient_circuits.get(id(circuit)): - gradient_circuit, base_parameter_values_all = self._gradient_circuits[ - id(circuit) - ] + gradient_circuit, base_parameter_values_all = self._gradient_circuits[id(circuit)] else: - gradient_circuit, base_parameter_values_all = param_shift_preprocessing( - circuit - ) + gradient_circuit, base_parameter_values_all = param_shift_preprocessing(circuit) self._gradient_circuits[id(circuit)] = ( gradient_circuit, base_parameter_values_all, @@ -102,8 +98,12 @@ def _run( for idx, coeff, dist_plus, dist_minus in zip( result_indices_all[i], coeffs_all[i], result.quasi_dists[:n], result.quasi_dists[n:] ): - grad_dists[idx][list(dist_plus.keys())] += (np.array(list(dist_plus.values())) * coeff) - grad_dists[idx][list(dist_minus.keys())] -= (np.array(list(dist_minus.values())) * coeff) + grad_dists[idx][list(dist_plus.keys())] += ( + np.array(list(dist_plus.values())) * coeff + ) + grad_dists[idx][list(dist_minus.keys())] -= ( + np.array(list(dist_minus.values())) * coeff + ) gradient_ = [] for grad_dist in grad_dists: diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 164494045eb2..79340731fd51 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -240,9 +240,7 @@ def make_param_shift_parameter_values( """ circuit = gradient_circuit_data.circuit gradient_circuit = gradient_circuit_data.gradient_circuit - gradient_parameter_values = np.zeros( - len(gradient_circuit_data.gradient_circuit.parameters) - ) + gradient_parameter_values = np.zeros(len(gradient_circuit_data.gradient_circuit.parameters)) plus_offsets, minus_offsets, result_indices, coeffs = [], [], [], [] result_idx = 0 for i, param in enumerate(circuit.parameters): @@ -252,8 +250,7 @@ def make_param_shift_parameter_values( if param in param_set: plus_offsets.extend(base_parameter_values[idx] for idx in indices) minus_offsets.extend( - base_parameter_values[idx + len(gradient_circuit.parameters)] - for idx in indices + base_parameter_values[idx + len(gradient_circuit.parameters)] for idx in indices ) result_indices.extend(result_idx for _ in range(len(indices))) result_idx += 1 @@ -280,7 +277,6 @@ def make_param_shift_parameter_values( return gradient_parameter_values_plus, gradient_parameter_values_minus, result_indices, coeffs - @dataclass class LinearCombGradientCircuit: """Gradient circuit for the linear combination of unitaries method. diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index eed4e39b56f9..f0423b05958c 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -20,10 +20,12 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import (FiniteDiffEstimatorGradient, - LinCombEstimatorGradient, - ParamShiftEstimatorGradient, - SPSAEstimatorGradient) +from qiskit.algorithms.gradients import ( + FiniteDiffEstimatorGradient, + LinCombEstimatorGradient, + ParamShiftEstimatorGradient, + SPSAEstimatorGradient, +) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.circuit.library.standard_gates.rxx import RXXGate diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 9fd3ee0cd4aa..fb03a71a79d2 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -20,10 +20,12 @@ from ddt import ddt from qiskit import QuantumCircuit -from qiskit.algorithms.gradients import (FiniteDiffSamplerGradient, - LinCombSamplerGradient, - ParamShiftSamplerGradient, - SPSASamplerGradient) +from qiskit.algorithms.gradients import ( + FiniteDiffSamplerGradient, + LinCombSamplerGradient, + ParamShiftSamplerGradient, + SPSASamplerGradient, +) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.circuit.library.standard_gates.rxx import RXXGate From d7f80db65a7ac24ebea2f1ccbe1cfae1a0321775 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 31 Aug 2022 12:38:38 +0900 Subject: [PATCH 17/37] fix epsilon and doc --- qiskit/algorithms/gradients/__init__.py | 8 ++-- .../finite_diff_estimator_gradient.py | 9 +++- .../gradients/finite_diff_sampler_gradient.py | 10 +++- .../algorithms/test_estimator_gradient.py | 48 +++++++++++++++---- .../algorithms/test_sampler_gradient.py | 48 +++++++++++++++---- 5 files changed, 96 insertions(+), 27 deletions(-) diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py index 99584a80171a..473ef767930b 100644 --- a/qiskit/algorithms/gradients/__init__.py +++ b/qiskit/algorithms/gradients/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -===================================== +============================================== Gradients (:mod:`qiskit.algorithms.gradients`) -===================================== +============================================== .. currentmodule:: qiskit.algorithms.gradients @@ -27,7 +27,7 @@ BaseEstimatorGradient Estimator Gradients -========= +=================== .. autosummary:: :toctree: ../stubs/ @@ -38,7 +38,7 @@ SPSAEstimatorGradient Sampler Gradients -========= +================= .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 4f0a691f2323..959acfee1ba0 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -32,7 +32,7 @@ class FiniteDiffEstimatorGradient(BaseEstimatorGradient): Compute the gradients of the expectation values by finite difference method. """ - def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_options): + def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): """ Args: estimator: The estimator used to compute the gradients. @@ -40,7 +40,14 @@ def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6, **run_option run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. + + Raises: + ValueError: If `epsilon` is not float. """ + if not isinstance(epsilon, float): + raise ValueError( + f"epsilon must be a float, but got {type(epsilon)} instead." + ) self._epsilon = epsilon self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 92a33562a4ff..fc19cccfaa6f 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -31,7 +31,7 @@ class FiniteDiffSamplerGradient(BaseSamplerGradient): def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-6, + epsilon: float, **run_options, ): """ @@ -41,8 +41,14 @@ def __init__( run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. - """ + Raises: + ValueError: If `epsilon` is not float. + """ + if not isinstance(epsilon, float): + raise ValueError( + f"epsilon must be a float, but got {type(epsilon)} instead." + ) self._epsilon = epsilon super().__init__(sampler, **run_options) diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index f0423b05958c..65c35b30f9fd 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -52,7 +52,10 @@ def test_gradient_p(self, grad): qc.h(0) qc.p(a, 0) qc.h(0) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) op = SparsePauliOp.from_list([("Z", 1)]) param_list = [[np.pi / 4], [0], [np.pi / 2]] correct_results = [[-1 / np.sqrt(2)], [0], [-1]] @@ -74,7 +77,10 @@ def test_gradient_u(self, grad): qc.h(0) qc.u(a, b, c, 0) qc.h(0) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) op = SparsePauliOp.from_list([("Z", 1)]) param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] @@ -92,7 +98,10 @@ def test_gradient_efficient_su2(self, grad): estimator = Estimator() qc = EfficientSU2(2, reps=1) op = SparsePauliOp.from_list([("ZI", 1)]) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) param_list = [ [np.pi / 4 for param in qc.parameters], [np.pi / 2 for param in qc.parameters], @@ -130,7 +139,10 @@ def test_gradient_2qubit_gate(self, grad): for i, param in enumerate(param_list): a = Parameter("a") qc = QuantumCircuit(2) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) if gate is RZZGate: qc.h([0, 1]) @@ -153,7 +165,10 @@ def test_gradient_parameter_coefficient(self, grad): qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) qc.p(2 * qc.parameters[0] + 1, 0) qc.rxx(qc.parameters[0] + 2, 0, 1) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] correct_results = [ [-0.7266653, -0.4905135, -0.0068606, -0.9228880], @@ -175,7 +190,10 @@ def test_gradient_parameters(self, grad): qc = QuantumCircuit(1) qc.rx(a, 0) qc.rx(b, 0) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ [-0.70710678], @@ -197,7 +215,10 @@ def test_gradient_multi_arguments(self, grad): qc.rx(a, 0) qc2 = QuantumCircuit(1) qc2.rx(b, 0) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) param_list = [[np.pi / 4], [np.pi / 2]] correct_results = [ [-0.70710678], @@ -235,11 +256,18 @@ def test_gradient_validation(self, grad): a = Parameter("a") qc = QuantumCircuit(1) qc.rx(a, 0) - gradient = grad(estimator) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + with self.assertRaises(ValueError): + _ = grad(Sampler(), epsilon=1e-6) + with self.assertRaises(ValueError): + _ = grad(estimator, epsilon='1e-6') + else: + gradient = grad(estimator) + with self.assertRaises(ValueError): + _ = grad(Sampler()) param_list = [[np.pi / 4], [np.pi / 2]] op = SparsePauliOp.from_list([("Z", 1)]) - with self.assertRaises(ValueError): - _ = grad(Sampler()) with self.assertRaises(ValueError): gradient.run([qc], [op], param_list) with self.assertRaises(ValueError): diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index fb03a71a79d2..e536c50f03bb 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -50,7 +50,10 @@ def test_gradient_p(self, grad): qc.p(a, 0) qc.h(0) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [[np.pi / 4], [0], [np.pi / 2]] correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], @@ -75,7 +78,10 @@ def test_gradient_u(self, grad): qc.u(a, b, c, 0) qc.h(0) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {0: 0, 1: 0}, {0: 0, 1: 0}], @@ -93,7 +99,10 @@ def test_gradient_efficient_su2(self, grad): sampler = Sampler() qc = EfficientSU2(2, reps=1) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [ [np.pi / 4 for param in qc.parameters], [np.pi / 2 for param in qc.parameters], @@ -203,7 +212,10 @@ def test_gradient_2qubit_gate(self, grad): qc = QuantumCircuit(2) qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) gradients = gradient.run([qc], [param]).result().gradients[0] for j, quasi_dist in enumerate(gradients): for k in quasi_dist: @@ -220,7 +232,10 @@ def test_gradient_parameter_coefficient(self, grad): qc.p(2 * qc.parameters[0] + 1, 0) qc.rxx(qc.parameters[0] + 2, 0, 1) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] correct_results = [ [ @@ -293,7 +308,10 @@ def test_gradient_parameters(self, grad): qc.rx(a, 0) qc.rz(b, 0) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], @@ -316,7 +334,10 @@ def test_gradient_multi_arguments(self, grad): qc2 = QuantumCircuit(1) qc2.rx(b, 0) qc2.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) param_list = [[np.pi / 4], [np.pi / 2]] correct_results = [ [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], @@ -357,10 +378,17 @@ def test_gradient_validation(self, grad): qc = QuantumCircuit(1) qc.rx(a, 0) qc.measure_all() - gradient = grad(sampler) + if grad is FiniteDiffSamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + with self.assertRaises(ValueError): + _ = grad(Estimator(), epsilon=1e-6) + with self.assertRaises(ValueError): + _ = grad(sampler, epsilon='1e-6') + else: + gradient = grad(sampler) + with self.assertRaises(ValueError): + _ = grad(Estimator()) param_list = [[np.pi / 4], [np.pi / 2]] - with self.assertRaises(ValueError): - _ = grad(Estimator()) with self.assertRaises(ValueError): gradient.run([qc], param_list) with self.assertRaises(ValueError): From 0433f9a5c57e040c4ec008f260b113c74cdb5143 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Wed, 31 Aug 2022 12:55:48 +0900 Subject: [PATCH 18/37] lint Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/finite_diff_estimator_gradient.py | 4 +--- .../gradients/finite_diff_sampler_gradient.py | 4 +--- qiskit/algorithms/gradients/utils.py | 11 +++++------ test/python/algorithms/test_estimator_gradient.py | 2 +- test/python/algorithms/test_sampler_gradient.py | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 959acfee1ba0..ff52a2dca159 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -45,9 +45,7 @@ def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): ValueError: If `epsilon` is not float. """ if not isinstance(epsilon, float): - raise ValueError( - f"epsilon must be a float, but got {type(epsilon)} instead." - ) + raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") self._epsilon = epsilon self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index fc19cccfaa6f..3b38d878de2b 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -46,9 +46,7 @@ def __init__( ValueError: If `epsilon` is not float. """ if not isinstance(epsilon, float): - raise ValueError( - f"epsilon must be a float, but got {type(epsilon)} instead." - ) + raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") self._epsilon = epsilon super().__init__(sampler, **run_options) diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index 79340731fd51..d68108898fd2 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -21,7 +21,6 @@ from collections import defaultdict from copy import deepcopy from dataclasses import dataclass -from typing import Dict, List import numpy as np @@ -67,9 +66,9 @@ class ParameterShiftGradientCircuit: circuit: QuantumCircuit gradient_circuit: QuantumCircuit - gradient_parameter_map: Dict[Parameter, Parameter] - gradient_virtual_parameter_map: Dict[Parameter, Parameter] - coeff_map: Dict[Parameter, float | ParameterExpression] + gradient_parameter_map: dict[Parameter, Parameter] + gradient_virtual_parameter_map: dict[Parameter, Parameter] + coeff_map: dict[Parameter, float | ParameterExpression] def make_param_shift_gradient_circuit_data( @@ -177,7 +176,7 @@ def make_param_shift_gradient_circuit_data( def make_param_shift_base_parameter_values( gradient_circuit_data: ParameterShiftGradientCircuit, -) -> List[np.ndarray]: +) -> list[np.ndarray]: """Makes base parameter values for the parameter shift method. Each base parameter value will be added to the given parameter values in later calculations. @@ -293,7 +292,7 @@ class LinearCombGradientCircuit: def make_lin_comb_gradient_circuit( circuit: QuantumCircuit, add_measurement: bool = False -) -> Dict[Parameter, List[LinearCombGradientCircuit]]: +) -> dict[Parameter, list[LinearCombGradientCircuit]]: """Makes gradient circuits for the linear combination of unitaries method. Args: diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 65c35b30f9fd..e09ac78d014e 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -261,7 +261,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(ValueError): _ = grad(Sampler(), epsilon=1e-6) with self.assertRaises(ValueError): - _ = grad(estimator, epsilon='1e-6') + _ = grad(estimator, epsilon="1e-6") else: gradient = grad(estimator) with self.assertRaises(ValueError): diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index e536c50f03bb..f174caef9f18 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -383,7 +383,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(ValueError): _ = grad(Estimator(), epsilon=1e-6) with self.assertRaises(ValueError): - _ = grad(sampler, epsilon='1e-6') + _ = grad(sampler, epsilon="1e-6") else: gradient = grad(sampler) with self.assertRaises(ValueError): From f12070ed79c27a3f1d66d945ac671a1053c1d6c7 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Thu, 1 Sep 2022 00:32:32 +0900 Subject: [PATCH 19/37] fix --- .../gradients/base_estimator_gradient.py | 10 +++++----- .../algorithms/gradients/base_sampler_gradient.py | 8 ++++---- .../gradients/finite_diff_estimator_gradient.py | 4 ++-- .../gradients/finite_diff_sampler_gradient.py | 4 ++-- .../gradients/lin_comb_estimator_gradient.py | 2 +- .../gradients/lin_comb_sampler_gradient.py | 2 +- .../gradients/param_shift_estimator_gradient.py | 2 +- .../gradients/param_shift_sampler_gradient.py | 2 +- .../gradients/spsa_estimator_gradient.py | 15 +++++++++++---- .../algorithms/gradients/spsa_sampler_gradient.py | 12 +++++++++--- test/python/algorithms/test_estimator_gradient.py | 6 ++++-- test/python/algorithms/test_sampler_gradient.py | 7 +++++-- 12 files changed, 46 insertions(+), 28 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index ec7a182387b5..ef9fd788b322 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -41,7 +41,7 @@ def __init__( Args: estimator: The estimator used to compute the gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. Raises: @@ -70,10 +70,10 @@ def run( parameter_values: The list of parameter values to be bound to the circuit. parameters: The Sequence of Sequence of Parameters to calculate only the gradients of the specified parameters. Each Sequence of Parameters corresponds to a circuit in - `circuits`. Defaults to None, which means that the gradients of all parameters in + ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. Returns: @@ -120,7 +120,7 @@ def _validate_arguments( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, ) -> None: - """Validate the arguments of the `evaluate` method. + """Validate the arguments of the ``run`` method. Args: circuits: The list of quantum circuits to compute the gradients. @@ -128,7 +128,7 @@ def _validate_arguments( parameter_values: The list of parameter values to be bound to the circuit. parameters: The Sequence of Sequence of Parameters to calculate only the gradients of the specified parameters. Each Sequence of Parameters corresponds to a circuit in - `circuits`. Defaults to None, which means that the gradients of all parameters in + ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. Raises: diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 61dbdf00b040..ab1af968d95c 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -61,10 +61,10 @@ def run( parameter_values: The list of parameter values to be bound to the circuit. parameters: The Sequence of Sequence of Parameters to calculate only the gradients of the specified parameters. Each Sequence of Parameters corresponds to a circuit in - `circuits`. Defaults to None, which means that the gradients of all parameters in + ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. Returns: @@ -106,14 +106,14 @@ def _validate_arguments( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, ): - """Validate the arguments of the `evaluate` method. + """Validate the arguments of the ``run`` method. Args: circuits: The list of quantum circuits to compute the gradients. parameter_values: The list of parameter values to be bound to the circuit. parameters: The Sequence of Sequence of Parameters to calculate only the gradients of the specified parameters. Each Sequence of Parameters corresponds to a circuit in - `circuits`. Defaults to None, which means that the gradients of all parameters in + ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. Raises: diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index ff52a2dca159..923269539db2 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -38,11 +38,11 @@ def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If `epsilon` is not float. + ValueError: If ``epsilon`` is not float. """ if not isinstance(epsilon, float): raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 3b38d878de2b..b65e79c87252 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -39,11 +39,11 @@ def __init__( sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If `epsilon` is not float. + ValueError: If ``epsilon`` is not float. """ if not isinstance(epsilon, float): raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 750ef97a9730..dc278875aebb 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -46,7 +46,7 @@ def __init__(self, estimator: BaseEstimator, **run_options): Args: estimator: The estimator used to compute the gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuits = {} diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 08b0f2839258..aa8827d8b4fb 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -41,7 +41,7 @@ def __init__(self, sampler: BaseSampler, **run_options): Args: sampler: The sampler used to compute the gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 0c962d5fcb63..cc7a9160c3f8 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -38,7 +38,7 @@ def __init__(self, estimator: BaseEstimator, **run_options): Args: estimator: The estimator used to compute the gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuits = {} diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index b5a10a856d70..58da999755a7 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -35,7 +35,7 @@ def __init__(self, sampler: BaseSampler, **run_options): Args: sampler: The sampler used to compute the gradients. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default + run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. """ self._gradient_circuits = {} diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index b034754f54a7..80e4a6161019 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -36,18 +36,25 @@ class SPSAEstimatorGradient(BaseEstimatorGradient): def __init__( self, estimator: BaseEstimator, - epsilon: float = 1e-6, + epsilon: float, seed: int | None = None, **run_options, ): """ Args: estimator: The estimator used to compute the gradients. - epsilon: The offset size for the finite difference gradients. + epsilon: The offset size for the SPSA gradients. seed: The seed for a random perturbation vector. run_options: Backend runtime options used for circuit execution. The order of priority is: - run_options in `run` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting.""" + run_options in ``run`` method > gradient's default run_options > primitive's default + setting. Higher priority setting overrides lower priority setting. + + Raises: + ValueError: If ``epsilon`` is not float. + """ + if not isinstance(epsilon, float): + raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") + self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index c243f2f5dcd3..5cedae0917ed 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -34,18 +34,24 @@ class SPSASamplerGradient(BaseSamplerGradient): def __init__( self, sampler: BaseSampler, - epsilon: float = 1e-6, + epsilon: float, seed: int | None = None, **run_options, ): """ Args: sampler: The sampler used to compute the gradients. - epsilon: The offset size for the finite difference gradients. + epsilon: The offset size for the SPSA gradients. seed: The seed for a random perturbation vector. run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default - setting. Higher priority setting overrides lower priority setting.""" + setting. Higher priority setting overrides lower priority setting. + + Raises: + ValueError: If ``epsilon`` is not float. + """ + if not isinstance(epsilon, float): + raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index e09ac78d014e..8806c99f1584 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -280,6 +280,8 @@ def test_gradient_validation(self, grad): def test_spsa_gradient(self): """Test the SPSA estimator gradient""" estimator = Estimator() + with self.assertRaises(ValueError): + _ = SPSAEstimatorGradient(estimator, epsilon="1e-6") a = Parameter("a") b = Parameter("b") qc = QuantumCircuit(2) @@ -288,12 +290,12 @@ def test_spsa_gradient(self): param_list = [[1, 1]] correct_results = [[-0.84147098, 0.84147098]] op = SparsePauliOp.from_list([("ZI", 1)]) - gradient = SPSAEstimatorGradient(estimator, seed=123) + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) gradients = gradient.run([qc], [op], param_list).result().gradients np.testing.assert_almost_equal(gradients, correct_results, 3) # multi parameters - gradient = SPSAEstimatorGradient(estimator, seed=123) + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) param_list2 = [[1, 1], [1, 1], [3, 3]] gradients2 = ( gradient.run([qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None]) diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index f174caef9f18..5ace1e6958d7 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -399,6 +399,9 @@ def test_gradient_validation(self, grad): def test_spsa_gradient(self): """Test the SPSA sampler gradient""" sampler = Sampler() + with self.assertRaises(ValueError): + _ = SPSASamplerGradient(sampler, epsilon="1e-6") + a = Parameter("a") b = Parameter("b") qc = QuantumCircuit(2) @@ -412,7 +415,7 @@ def test_spsa_gradient(self): {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, ], ] - gradient = SPSASamplerGradient(sampler, seed=123) + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) for i, param in enumerate(param_list): gradients = gradient.run([qc], [param]).result().gradients[0] for j, quasi_dist in enumerate(gradients): @@ -433,7 +436,7 @@ def test_spsa_gradient(self): {0: 0.0141129, 1: 0.0564471, 2: 0.3642884, 3: -0.4348484}, ], ] - gradient = SPSASamplerGradient(sampler, seed=123) + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) gradients = ( gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().gradients ) From c961b5daef388eade83855df45c98515c00d771e Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:17:39 +0900 Subject: [PATCH 20/37] Update qiskit/algorithms/gradients/base_sampler_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_sampler_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index ab1af968d95c..102e90014ded 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -38,7 +38,7 @@ def __init__(self, sampler: BaseSampler, **run_options): setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If the sampler is not an instance of BaseEstimator. + ValueError: If the sampler is not an instance of ``BaseSampler``. """ if not isinstance(sampler, BaseSampler): raise ValueError( From 08b8a72660eb4af75edfc0c0a02cd96c209e7d47 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:18:04 +0900 Subject: [PATCH 21/37] Update qiskit/algorithms/gradients/base_sampler_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_sampler_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 102e90014ded..90752ab7effa 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -27,7 +27,7 @@ class BaseSamplerGradient(ABC): - """Base class for a SamplerGradient to compute the gradients of the sampling probability.""" + """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" def __init__(self, sampler: BaseSampler, **run_options): """ From fa51f86685dd40923932b92a2a319dec03645743 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:18:14 +0900 Subject: [PATCH 22/37] Update qiskit/algorithms/gradients/base_estimator_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_estimator_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index ef9fd788b322..b9ae654feb98 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -30,7 +30,7 @@ class BaseEstimatorGradient(ABC): - """Base class for an EstimatorGradient to compute the gradients of the expectation value.""" + """Base class for an ``EstimatorGradient`` to compute the gradients of the expectation value.""" def __init__( self, From f400e72e559f249b6f41e24060f3340cac15104e Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:18:24 +0900 Subject: [PATCH 23/37] Update qiskit/algorithms/gradients/base_sampler_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_sampler_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index 90752ab7effa..f03e935ffc9d 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ -Abstract Base class of Gradient for Sampler. +Abstract base class of gradient for ``Sampler``. """ from __future__ import annotations From dfc3bb06d870447622ca478418d35243f6e70f97 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:18:34 +0900 Subject: [PATCH 24/37] Update qiskit/algorithms/gradients/base_estimator_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_estimator_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index b9ae654feb98..6485a9759cb8 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """ -Abstract Base class of Gradient for Estimator. +Abstract base class of gradient for ``Estimator``. """ from __future__ import annotations From 9f748a00efea1c7ec662811053763159fddcee09 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:18:46 +0900 Subject: [PATCH 25/37] Update qiskit/algorithms/gradients/base_estimator_gradient.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/base_estimator_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index 6485a9759cb8..fd8c5c692887 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -45,7 +45,7 @@ def __init__( setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If the estimator is not an instance of BaseEstimator. + ValueError: If the estimator is not an instance of ``BaseEstimator``. """ if not isinstance(estimator, BaseEstimator): raise ValueError( From 84f0ee5973d2748d6fbb8ae84d4cd4d50e1ea664 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Thu, 1 Sep 2022 18:30:15 +0900 Subject: [PATCH 26/37] change epsilon error Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../algorithms/gradients/finite_diff_estimator_gradient.py | 6 +++--- .../algorithms/gradients/finite_diff_sampler_gradient.py | 6 +++--- qiskit/algorithms/gradients/spsa_estimator_gradient.py | 7 +++---- qiskit/algorithms/gradients/spsa_sampler_gradient.py | 6 +++--- test/python/algorithms/test_estimator_gradient.py | 4 ++-- test/python/algorithms/test_sampler_gradient.py | 4 ++-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 923269539db2..55d33d71f86e 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -42,10 +42,10 @@ def __init__(self, estimator: BaseEstimator, epsilon: float, **run_options): setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If ``epsilon`` is not float. + ValueError: If ``epsilon`` is not positive. """ - if not isinstance(epsilon, float): - raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._base_parameter_values_dict = {} super().__init__(estimator, **run_options) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index b65e79c87252..fd238d6b8b9b 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -43,10 +43,10 @@ def __init__( setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If ``epsilon`` is not float. + ValueError: If ``epsilon`` is not positive. """ - if not isinstance(epsilon, float): - raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon super().__init__(sampler, **run_options) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 80e4a6161019..f149643d874a 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -50,11 +50,10 @@ def __init__( setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If ``epsilon`` is not float. + ValueError: If ``epsilon`` is not positive. """ - if not isinstance(epsilon, float): - raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") - + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 5cedae0917ed..9615b0b33da3 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -48,10 +48,10 @@ def __init__( setting. Higher priority setting overrides lower priority setting. Raises: - ValueError: If ``epsilon`` is not float. + ValueError: If ``epsilon`` is not positive. """ - if not isinstance(epsilon, float): - raise ValueError(f"epsilon must be a float, but got {type(epsilon)} instead.") + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 8806c99f1584..79466f7e2867 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -261,7 +261,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(ValueError): _ = grad(Sampler(), epsilon=1e-6) with self.assertRaises(ValueError): - _ = grad(estimator, epsilon="1e-6") + _ = grad(estimator, epsilon=-0.1) else: gradient = grad(estimator) with self.assertRaises(ValueError): @@ -281,7 +281,7 @@ def test_spsa_gradient(self): """Test the SPSA estimator gradient""" estimator = Estimator() with self.assertRaises(ValueError): - _ = SPSAEstimatorGradient(estimator, epsilon="1e-6") + _ = SPSAEstimatorGradient(estimator, epsilon=-0.1) a = Parameter("a") b = Parameter("b") qc = QuantumCircuit(2) diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 5ace1e6958d7..556da2e2cd19 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -383,7 +383,7 @@ def test_gradient_validation(self, grad): with self.assertRaises(ValueError): _ = grad(Estimator(), epsilon=1e-6) with self.assertRaises(ValueError): - _ = grad(sampler, epsilon="1e-6") + _ = grad(sampler, epsilon=-0.1) else: gradient = grad(sampler) with self.assertRaises(ValueError): @@ -400,7 +400,7 @@ def test_spsa_gradient(self): """Test the SPSA sampler gradient""" sampler = Sampler() with self.assertRaises(ValueError): - _ = SPSASamplerGradient(sampler, epsilon="1e-6") + _ = SPSASamplerGradient(sampler, epsilon=-0.1) a = Parameter("a") b = Parameter("b") From 618c90bc77c38d15d1052d9a7ef450d611296729 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:31:00 +0900 Subject: [PATCH 27/37] Update qiskit/algorithms/gradients/estimator_gradient_result.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/estimator_gradient_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index ebfedf54b3f3..5fa78b570995 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -23,7 +23,7 @@ @dataclass(frozen=True) class EstimatorGradientResult: - """Result of EstimatorGradient. + """Result of ``EstimatorGradient``. Args: gradients: The gradients of the expectation values. From e8b6f4fa947d5a7a07658164ce4a019e0d25a855 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:31:09 +0900 Subject: [PATCH 28/37] Update qiskit/algorithms/gradients/sampler_gradient_result.py Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/sampler_gradient_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index e13214d65855..7734575b5d1f 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -23,7 +23,7 @@ @dataclass(frozen=True) class SamplerGradientResult: - """Result of SamplerGradient. + """Result of ``SamplerGradient``. Args: gradients: The gradients of the quasi distributions. From f73b0092cbb23906db1f6545f1b38b9870411040 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Thu, 1 Sep 2022 18:42:45 +0900 Subject: [PATCH 29/37] add gradient test --- .../algorithms/test_estimator_gradient.py | 47 ++++++++++++++-- .../algorithms/test_sampler_gradient.py | 54 +++++++++++++++++-- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 79466f7e2867..5e3856b0b901 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -28,12 +28,10 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.circuit.library.standard_gates.rxx import RXXGate -from qiskit.circuit.library.standard_gates.ryy import RYYGate -from qiskit.circuit.library.standard_gates.rzx import RZXGate -from qiskit.circuit.library.standard_gates.rzz import RZZGate +from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate from qiskit.primitives import Estimator, Sampler from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info.random import random_pauli_list from qiskit.test import QiskitTestCase @@ -306,6 +304,47 @@ def test_spsa_gradient(self): for grad, correct in zip(gradients2, correct_results2): np.testing.assert_almost_equal(grad, correct, 3) + @combine(grad=[ParamShiftEstimatorGradient, LinCombEstimatorGradient]) + def test_gradient_random_parameters(self, grad): + """Test param shift and lin comb w/ random parameters""" + rng = np.random.default_rng(123) + qc = RealAmplitudes(num_qubits=3, reps=1) + params = qc.parameters + qc.rx(3.0 * params[0] + params[1].sin(), 0) + qc.ry(params[0].exp() + 2 * params[1], 1) + qc.rz(params[0] * params[1] - params[2], 2) + qc.p(2 * params[0] + 1, 0) + qc.u(params[0].sin(), params[1] - 2, params[2] * params[3], 1) + qc.sx(2) + qc.rxx(params[0].sin(), 1, 2) + qc.ryy(params[1].cos(), 2, 0) + qc.rzz(params[2] * 2, 0, 1) + qc.crx(params[0].exp(), 1, 2) + qc.cry(params[1].arctan(), 2, 0) + qc.crz(params[2] * -2, 0, 1) + qc.dcx(0, 1) + qc.csdg(0, 1) + qc.toffoli(0, 1, 2) + qc.iswap(0, 2) + qc.swap(1, 2) + qc.global_phase = params[0] * params[1] + params[2].cos().exp() + + size = 10 + op = SparsePauliOp(random_pauli_list(num_qubits=qc.num_qubits, size=size, seed=rng)) + op.coeffs = rng.normal(0, 10, size) + + estimator = Estimator() + findiff = FiniteDiffEstimatorGradient(estimator, 1e-6) + gradient = grad(estimator) + + num_tries = 10 + param_values = rng.normal(0, 2, (num_tries, qc.num_parameters)).tolist() + np.testing.assert_allclose( + findiff.run([qc] * num_tries, [op] * num_tries, param_values).result().gradients, + gradient.run([qc] * num_tries, [op] * num_tries, param_values).result().gradients, + rtol=1e-4, + ) + if __name__ == "__main__": unittest.main() diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 556da2e2cd19..3d0a28fa4ca5 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -28,11 +28,9 @@ ) from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes -from qiskit.circuit.library.standard_gates.rxx import RXXGate -from qiskit.circuit.library.standard_gates.ryy import RYYGate -from qiskit.circuit.library.standard_gates.rzx import RZXGate -from qiskit.circuit.library.standard_gates.rzz import RZZGate +from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate from qiskit.primitives import Estimator, Sampler +from qiskit.result import QuasiDistribution from qiskit.test import QiskitTestCase @@ -446,6 +444,54 @@ def test_spsa_gradient(self): for k in q_dists: self.assertAlmostEqual(q_dists[k], correct_results2[i][j][k], 3) + @combine(grad=[ParamShiftSamplerGradient, LinCombSamplerGradient]) + def test_gradient_random_parameters(self, grad): + """Test param shift and lin comb w/ random parameters""" + rng = np.random.default_rng(123) + qc = RealAmplitudes(num_qubits=3, reps=1) + params = qc.parameters + qc.rx(3.0 * params[0] + params[1].sin(), 0) + qc.ry(params[0].exp() + 2 * params[1], 1) + qc.rz(params[0] * params[1] - params[2], 2) + qc.p(2 * params[0] + 1, 0) + qc.u(params[0].sin(), params[1] - 2, params[2] * params[3], 1) + qc.sx(2) + qc.rxx(params[0].sin(), 1, 2) + qc.ryy(params[1].cos(), 2, 0) + qc.rzz(params[2] * 2, 0, 1) + qc.crx(params[0].exp(), 1, 2) + qc.cry(params[1].arctan(), 2, 0) + qc.crz(params[2] * -2, 0, 1) + qc.dcx(0, 1) + qc.csdg(0, 1) + qc.toffoli(0, 1, 2) + qc.iswap(0, 2) + qc.swap(1, 2) + qc.global_phase = params[0] * params[1] + params[2].cos().exp() + qc.measure_all() + + sampler = Sampler() + findiff = FiniteDiffSamplerGradient(sampler, 1e-6) + gradient = grad(sampler) + + num_qubits = qc.num_qubits + num_tries = 10 + param_values = rng.normal(0, 2, (num_tries, qc.num_parameters)).tolist() + result1 = findiff.run([qc] * num_tries, param_values).result().gradients + result2 = gradient.run([qc] * num_tries, param_values).result().gradients + self.assertEqual(len(result1), len(result2)) + for res1, res2 in zip(result1, result2): + array1 = _quasi2array(res1, num_qubits) + array2 = _quasi2array(res2, num_qubits) + np.testing.assert_allclose(array1, array2, rtol=1e-4) + + +def _quasi2array(quasis: list[QuasiDistribution], num_qubits: int) -> np.ndarray: + ret = np.zeros((len(quasis), 2**num_qubits)) + for i, quasi in enumerate(quasis): + ret[i, list(quasi.keys())] = list(quasi.values()) + return ret + if __name__ == "__main__": unittest.main() From a30e7b3f73931de78d83fdd1683682b5a7c76b6f Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 01:12:45 +0900 Subject: [PATCH 30/37] added batch size in spsa gradients Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/lin_comb_estimator_gradient.py | 8 +++- .../gradients/lin_comb_sampler_gradient.py | 4 +- .../param_shift_estimator_gradient.py | 6 +-- .../gradients/param_shift_sampler_gradient.py | 6 +-- .../gradients/sampler_gradient_result.py | 6 +-- .../gradients/spsa_estimator_gradient.py | 26 ++++++++--- .../gradients/spsa_sampler_gradient.py | 44 ++++++++++++------- qiskit/algorithms/gradients/utils.py | 14 +++--- .../algorithms/test_estimator_gradient.py | 26 ++++++----- .../algorithms/test_sampler_gradient.py | 25 +++++++++++ 10 files changed, 112 insertions(+), 53 deletions(-) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index dc278875aebb..8f7501ec6e08 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -22,12 +22,14 @@ from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.primitives.utils import init_observable from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult -from .utils import make_lin_comb_gradient_circuit +from .utils import _make_lin_comb_gradient_circuit + Pauli_Z = Pauli("Z") @@ -65,6 +67,8 @@ def _run( for circuit, observable, parameter_values_, parameters_ in zip( circuits, observables, parameter_values, parameters ): + # Make the observable as observable as :class:`~qiskit.quantum_info.SparsePauliOp`. + observables = init_observable(observable) # a set of parameters to be differentiated if parameters_ is None: param_set = set(circuit.parameters) @@ -76,7 +80,7 @@ def _run( observable_ = observable.expand(Pauli_Z) gradient_circuits_ = self._gradient_circuits.get(id(circuit)) if gradient_circuits_ is None: - gradient_circuits_ = make_lin_comb_gradient_circuit(circuit) + gradient_circuits_ = _make_lin_comb_gradient_circuit(circuit) self._gradient_circuits[id(circuit)] = gradient_circuits_ # only compute the gradients for parameters in the parameter set diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index aa8827d8b4fb..746d9db3922f 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -24,7 +24,7 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult -from .utils import make_lin_comb_gradient_circuit +from .utils import _make_lin_comb_gradient_circuit class LinCombSamplerGradient(BaseSamplerGradient): @@ -68,7 +68,7 @@ def _run( # TODO: support measurement in different basis (Y and Z+iY) gradient_circuits_ = self._gradient_circuits.get(id(circuit)) if gradient_circuits_ is None: - gradient_circuits_ = make_lin_comb_gradient_circuit(circuit, add_measurement=True) + gradient_circuits_ = _make_lin_comb_gradient_circuit(circuit, add_measurement=True) self._gradient_circuits[id(circuit)] = gradient_circuits_ # only compute the gradients for parameters in the parameter set diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index cc7a9160c3f8..b5f1fe78c7f7 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -27,7 +27,7 @@ from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult -from .utils import param_shift_preprocessing, make_param_shift_parameter_values +from .utils import _param_shift_preprocessing, _make_param_shift_parameter_values class ParamShiftEstimatorGradient(BaseEstimatorGradient): @@ -67,7 +67,7 @@ def _run( if self._gradient_circuits.get(id(circuit)): gradient_circuit, base_parameter_values_all = self._gradient_circuits[id(circuit)] else: - gradient_circuit, base_parameter_values_all = param_shift_preprocessing(circuit) + gradient_circuit, base_parameter_values_all = _param_shift_preprocessing(circuit) self._gradient_circuits[id(circuit)] = ( gradient_circuit, base_parameter_values_all, @@ -78,7 +78,7 @@ def _run( gradient_parameter_values_minus, result_indices, coeffs, - ) = make_param_shift_parameter_values( + ) = _make_param_shift_parameter_values( gradient_circuit_data=gradient_circuit, base_parameter_values=base_parameter_values_all, parameter_values=parameter_values_, diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 58da999755a7..2446d477d435 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -24,7 +24,7 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult -from .utils import param_shift_preprocessing, make_param_shift_parameter_values +from .utils import _param_shift_preprocessing, _make_param_shift_parameter_values class ParamShiftSamplerGradient(BaseSamplerGradient): @@ -61,7 +61,7 @@ def _run( if self._gradient_circuits.get(id(circuit)): gradient_circuit, base_parameter_values_all = self._gradient_circuits[id(circuit)] else: - gradient_circuit, base_parameter_values_all = param_shift_preprocessing(circuit) + gradient_circuit, base_parameter_values_all = _param_shift_preprocessing(circuit) self._gradient_circuits[id(circuit)] = ( gradient_circuit, base_parameter_values_all, @@ -72,7 +72,7 @@ def _run( gradient_parameter_values_minus, result_indices, coeffs, - ) = make_param_shift_parameter_values( + ) = _make_param_shift_parameter_values( gradient_circuit_data=gradient_circuit, base_parameter_values=base_parameter_values_all, parameter_values=parameter_values_, diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index e13214d65855..1a77286354d4 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -18,20 +18,18 @@ from typing import Any from dataclasses import dataclass -from qiskit.result import QuasiDistribution - @dataclass(frozen=True) class SamplerGradientResult: """Result of SamplerGradient. Args: - gradients: The gradients of the quasi distributions. + gradients: The gradients of the sampling probabilities. metadata: Additional information about the job. run_options: run_options for the sampler. Currently, sampler's default run_options is not included. """ - gradients: list[list[QuasiDistribution]] + gradients: list[list[dict[int: float]]] metadata: list[dict[str, Any]] run_options: dict[str, Any] diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index f149643d874a..57261f2f7c78 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -37,6 +37,7 @@ def __init__( self, estimator: BaseEstimator, epsilon: float, + batch_size: int = 1, seed: int | None = None, **run_options, ): @@ -44,6 +45,7 @@ def __init__( Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the SPSA gradients. + batch_size: The number of gradients to average. seed: The seed for a random perturbation vector. run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in ``run`` method > gradient's default run_options > primitive's default @@ -55,6 +57,7 @@ def __init__( if epsilon <= 0: raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon + self._batch_size = batch_size self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() super().__init__(estimator, **run_options) @@ -79,12 +82,21 @@ def _run( indices = [circuit.parameters.data.index(p) for p in parameters_] metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) - offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) - plus = parameter_values_ + self._epsilon * offset - minus = parameter_values_ - self._epsilon * offset + offset = [ + (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) + for _ in range(self._batch_size) + ] + + plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] + minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] offsets.append(offset) - job = self._estimator.run([circuit] * 2, [observable] * 2, [plus, minus], **run_options) + job = self._estimator.run( + [circuit] * 2 * self._batch_size, + [observable] * 2 * self._batch_size, + plus + minus, + **run_options, + ) jobs.append(job) # combine the results @@ -92,7 +104,11 @@ def _run( gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient = (result.values[:n] - result.values[n:]) / (2 * self._epsilon * offsets[i]) + gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) + # take the average of the spsa gradients + gradient = np.average( + np.array([grad / offset for grad, offset in zip(gradient_, offsets[i])]), axis=0 + ) indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] gradients.append(gradient[indices]) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 9615b0b33da3..add8e3f3ef14 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -35,6 +35,7 @@ def __init__( self, sampler: BaseSampler, epsilon: float, + batch_size: int = 1, seed: int | None = None, **run_options, ): @@ -42,6 +43,7 @@ def __init__( Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the SPSA gradients. + batch_size: number of gradients to average. seed: The seed for a random perturbation vector. run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default @@ -52,6 +54,7 @@ def __init__( """ if epsilon <= 0: raise ValueError(f"epsilon ({epsilon}) should be positive.") + self._batch_size = batch_size self._epsilon = epsilon self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() @@ -74,33 +77,40 @@ def _run( indices = [circuit.parameters.data.index(p) for p in parameters_] metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) - offset = (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) - - plus = parameter_values_ + self._epsilon * offset - minus = parameter_values_ - self._epsilon * offset + offset = np.array( + [ + (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) + for _ in range(self._batch_size) + ] + ) + plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] + minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] offsets.append(offset) - job = self._sampler.run([circuit] * 2, [plus, minus], **run_options) + job = self._sampler.run([circuit] * 2 * self._batch_size, plus + minus, **run_options) jobs.append(job) # combine the results results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): - grad_dists = np.zeros(2 ** circuits[i].num_qubits) - dist_plus = result.quasi_dists[0] - dist_minus = result.quasi_dists[1] - grad_dists[list(dist_plus.keys())] += list(dist_plus.values()) - grad_dists[list(dist_minus.keys())] -= list(dist_minus.values()) + grad_dists = np.zeros((self._batch_size, 2 ** circuits[i].num_qubits)) + for j, (dist_plus, dist_minus) in enumerate( + zip(result.quasi_dists[: self._batch_size], result.quasi_dists[self._batch_size :]) + ): + grad_dists[j, list(dist_plus.keys())] += list(dist_plus.values()) + grad_dists[j, list(dist_minus.keys())] -= list(dist_minus.values()) grad_dists /= 2 * self._epsilon - - indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] gradient_ = [] - - gradient_.extend( - {k: offsets[i][j] * dist for k, dist in enumerate(grad_dists)} for j in indices - ) - + indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] + for j in range(circuits[i].num_parameters): + if not j in indices: + continue + grad = np.mean( + np.array([delta * dist for dist, delta in zip(grad_dists, offsets[i][:, j])]), + axis=0, + ) + gradient_.append(dict(enumerate(grad))) gradients.append(gradient_) # TODO: include primitive's run_options as well diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index d68108898fd2..c410dfe58ef2 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -71,7 +71,7 @@ class ParameterShiftGradientCircuit: coeff_map: dict[Parameter, float | ParameterExpression] -def make_param_shift_gradient_circuit_data( +def _make_param_shift_gradient_circuit_data( circuit: QuantumCircuit, ) -> ParameterShiftGradientCircuit: """Makes a gradient circuit data for the parameter shift method. This re-assigns each parameter in @@ -174,7 +174,7 @@ def make_param_shift_gradient_circuit_data( ) -def make_param_shift_base_parameter_values( +def _make_param_shift_base_parameter_values( gradient_circuit_data: ParameterShiftGradientCircuit, ) -> list[np.ndarray]: """Makes base parameter values for the parameter shift method. Each base parameter value will @@ -204,7 +204,7 @@ def make_param_shift_base_parameter_values( return plus_offsets + minus_offsets -def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradientCircuit: +def _param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradientCircuit: """Preprocessing for the parameter shift method. Args: @@ -213,13 +213,13 @@ def param_shift_preprocessing(circuit: QuantumCircuit) -> ParameterShiftGradient Returns: necessary data to calculate gradients with the parameter shift method. """ - gradient_circuit_data = make_param_shift_gradient_circuit_data(circuit) - base_parameter_values = make_param_shift_base_parameter_values(gradient_circuit_data) + gradient_circuit_data = _make_param_shift_gradient_circuit_data(circuit) + base_parameter_values = _make_param_shift_base_parameter_values(gradient_circuit_data) return gradient_circuit_data, base_parameter_values -def make_param_shift_parameter_values( +def _make_param_shift_parameter_values( gradient_circuit_data: ParameterShiftGradientCircuit, base_parameter_values: list[np.ndarray], parameter_values: np.ndarray, @@ -290,7 +290,7 @@ class LinearCombGradientCircuit: coeff: float | ParameterExpression -def make_lin_comb_gradient_circuit( +def _make_lin_comb_gradient_circuit( circuit: QuantumCircuit, add_measurement: bool = False ) -> dict[Parameter, list[LinearCombGradientCircuit]]: """Makes gradient circuits for the linear combination of unitaries method. diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 79466f7e2867..6b680f2d254f 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -121,7 +121,7 @@ def test_gradient_efficient_su2(self, grad): ] for i, param in enumerate(param_list): gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient], @@ -151,7 +151,7 @@ def test_gradient_2qubit_gate(self, grad): else: qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -177,7 +177,7 @@ def test_gradient_parameter_coefficient(self, grad): op = SparsePauliOp.from_list([("ZI", 1)]) for i, param in enumerate(param_list): gradients = gradient.run([qc], [op], [param]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -201,7 +201,7 @@ def test_gradient_parameters(self, grad): op = SparsePauliOp.from_list([("Z", 1)]) for i, param in enumerate(param_list): gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0] - np.testing.assert_almost_equal(gradients, correct_results[i], 3) + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -226,7 +226,7 @@ def test_gradient_multi_arguments(self, grad): ] op = SparsePauliOp.from_list([("Z", 1)]) gradients = gradient.run([qc, qc2], [op] * 2, param_list).result().gradients - np.testing.assert_almost_equal(gradients, correct_results, 3) + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) c = Parameter("c") qc3 = QuantumCircuit(1) @@ -243,9 +243,9 @@ def test_gradient_multi_arguments(self, grad): .result() .gradients ) - np.testing.assert_almost_equal(gradients2[0], correct_results2[0], 3) - np.testing.assert_almost_equal(gradients2[1], correct_results2[1], 3) - np.testing.assert_almost_equal(gradients2[2], correct_results2[2], 3) + np.testing.assert_allclose(gradients2[0], correct_results2[0], atol=1e-3) + np.testing.assert_allclose(gradients2[1], correct_results2[1], atol=1e-3) + np.testing.assert_allclose(gradients2[2], correct_results2[2], atol=1e-3) @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] @@ -292,7 +292,7 @@ def test_spsa_gradient(self): op = SparsePauliOp.from_list([("ZI", 1)]) gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) gradients = gradient.run([qc], [op], param_list).result().gradients - np.testing.assert_almost_equal(gradients, correct_results, 3) + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) # multi parameters gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) @@ -304,7 +304,13 @@ def test_spsa_gradient(self): ) correct_results2 = [[-0.84147098, 0.84147098], [0.84147098], [-0.14112001, 0.14112001]] for grad, correct in zip(gradients2, correct_results2): - np.testing.assert_almost_equal(grad, correct, 3) + np.testing.assert_allclose(grad, correct, atol=1e-3) + + # batch size + correct_results = [[-0.84147098, 0.1682942]] + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, batch_size=5, seed=123) + gradients = gradient.run([qc], [op], param_list).result().gradients + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) if __name__ == "__main__": diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 556da2e2cd19..a0761498b93c 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -446,6 +446,31 @@ def test_spsa_gradient(self): for k in q_dists: self.assertAlmostEqual(q_dists[k], correct_results2[i][j][k], 3) + # batch size + param_list = [[1, 1]] + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, batch_size=4, seed=123) + gradients = gradient.run([qc], param_list).result().gradients + correct_results3 = [ + [ + { + 0: -0.1620149622932887, + 1: -0.25872053011771756, + 2: 0.3723827084675668, + 3: 0.04835278392088804, + }, + { + 0: -0.1620149622932887, + 1: 0.3723827084675668, + 2: -0.25872053011771756, + 3: 0.04835278392088804, + }, + ] + ] + for i, result in enumerate(gradients): + for j, q_dists in enumerate(result): + for k in q_dists: + self.assertAlmostEqual(q_dists[k], correct_results3[i][j][k], 3) + if __name__ == "__main__": unittest.main() From b9a6586904fcb78eed78008725efc100c3a725fa Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 01:49:19 +0900 Subject: [PATCH 31/37] fix Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/base_estimator_gradient.py | 4 +-- .../gradients/base_sampler_gradient.py | 6 ++-- .../gradients/estimator_gradient_result.py | 13 ++++----- .../finite_diff_estimator_gradient.py | 5 ++++ .../gradients/finite_diff_sampler_gradient.py | 4 +++ .../gradients/lin_comb_estimator_gradient.py | 4 +++ .../gradients/lin_comb_sampler_gradient.py | 4 +++ .../param_shift_estimator_gradient.py | 4 +++ .../gradients/param_shift_sampler_gradient.py | 4 +++ .../gradients/sampler_gradient_result.py | 14 ++++----- .../gradients/spsa_estimator_gradient.py | 4 +++ .../gradients/spsa_sampler_gradient.py | 4 +++ qiskit/algorithms/gradients/utils.py | 29 ++++++------------- 13 files changed, 57 insertions(+), 42 deletions(-) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index fd8c5c692887..7b66225b7474 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -68,8 +68,8 @@ def run( circuits: The list of quantum circuits to compute the gradients. observables: The list of observables. parameter_values: The list of parameter values to be bound to the circuit. - parameters: The Sequence of Sequence of Parameters to calculate only the gradients of - the specified parameters. Each Sequence of Parameters corresponds to a circuit in + parameters: The sequence of parameters to calculate only the gradients of + the specified parameters. Each sequence of parameters corresponds to a circuit in ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. run_options: Backend runtime options used for circuit execution. The order of priority is: diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index f03e935ffc9d..f5e5d3e862bb 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -59,8 +59,8 @@ def run( Args: circuits: The list of quantum circuits to compute the gradients. parameter_values: The list of parameter values to be bound to the circuit. - parameters: The Sequence of Sequence of Parameters to calculate only the gradients of - the specified parameters. Each Sequence of Parameters corresponds to a circuit in + parameters: The sequence of parameters to calculate only the gradients of + the specified parameters. Each sequence of parameters corresponds to a circuit in ``circuits``. Defaults to None, which means that the gradients of all parameters in each circuit are calculated. run_options: Backend runtime options used for circuit execution. The order of priority is: @@ -105,7 +105,7 @@ def _validate_arguments( circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, - ): + ) -> None: """Validate the arguments of the ``run`` method. Args: diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/estimator_gradient_result.py index ebfedf54b3f3..10c6d74555f6 100644 --- a/qiskit/algorithms/gradients/estimator_gradient_result.py +++ b/qiskit/algorithms/gradients/estimator_gradient_result.py @@ -23,15 +23,12 @@ @dataclass(frozen=True) class EstimatorGradientResult: - """Result of EstimatorGradient. - - Args: - gradients: The gradients of the expectation values. - metadata: Additional information about the job. - run_options: run_options for the estimator. Currently, estimator's default run_options is not - included. - """ + """Result of EstimatorGradient.""" gradients: list[np.ndarray] + """The gradients of the expectation values.""" metadata: list[dict[str, Any]] + """Additional information about the job.""" run_options: dict[str, Any] + """run_options for the estimator. Currently, estimator's default run_options is not + included.""" diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 55d33d71f86e..b4e2e65041da 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -19,8 +19,10 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -81,6 +83,9 @@ def _run( jobs.append(job) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") + results = [job.result() for job in jobs] gradients = [] for result in results: diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index fd238d6b8b9b..b25511ac2efe 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -19,7 +19,9 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler +from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -74,6 +76,8 @@ def _run( jobs.append(job) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 8f7501ec6e08..7924c3d8f761 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -20,9 +20,11 @@ import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.primitives.utils import init_observable +from qiskit.providers import JobStatus from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -116,6 +118,8 @@ def _run( coeffs_all.append(coeffs) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 746d9db3922f..183779eebcd8 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -20,7 +20,9 @@ import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler +from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -101,6 +103,8 @@ def _run( coeffs_all.append(coeffs) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index b5f1fe78c7f7..09e708b1fcc4 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -20,8 +20,10 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -96,6 +98,8 @@ def _run( coeffs_all.append(coeffs) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index 2446d477d435..e42256bb71db 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -20,7 +20,9 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler +from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -90,6 +92,8 @@ def _run( coeffs_all.append(coeffs) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/sampler_gradient_result.py index 1a77286354d4..e8608e1af78a 100644 --- a/qiskit/algorithms/gradients/sampler_gradient_result.py +++ b/qiskit/algorithms/gradients/sampler_gradient_result.py @@ -21,15 +21,11 @@ @dataclass(frozen=True) class SamplerGradientResult: - """Result of SamplerGradient. + """Result of SamplerGradient.""" - Args: - gradients: The gradients of the sampling probabilities. - metadata: Additional information about the job. - run_options: run_options for the sampler. Currently, sampler's default run_options is not - included. - """ - - gradients: list[list[dict[int: float]]] + gradients: list[list[dict[int:float]]] + """The gradients of the sampling probabilities.""" metadata: list[dict[str, Any]] + """Additional information about the job.""" run_options: dict[str, Any] + """run_options for the sampler. Currently, sampler's default run_options is not included""" diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 57261f2f7c78..85919e51dee1 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -19,8 +19,10 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator +from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -100,6 +102,8 @@ def _run( jobs.append(job) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index add8e3f3ef14..c1836048832a 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -19,7 +19,9 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler +from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -91,6 +93,8 @@ def _run( jobs.append(job) # combine the results + if any(job.status() is not JobStatus.DONE for job in jobs): + raise QiskitError("The gradient job was not completed successfully. ") results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py index c410dfe58ef2..5c536fdf2173 100644 --- a/qiskit/algorithms/gradients/utils.py +++ b/qiskit/algorithms/gradients/utils.py @@ -50,25 +50,18 @@ @dataclass class ParameterShiftGradientCircuit: - """Stores gradient circuit data for the parameter shift method - - Args: - circuit (QuantumCircuit): The original quantum circuit - gradient_circuit (QuantumCircuit): An internal quantum circuit used to calculate the gradient - gradient_parameter_map (dict): A dictionary maps the parameters of ``circuit`` to - the parameters of ``gradient_circuit``. - gradient_virtual_parameter_map (dict): A dictionary maps the parameters of ``gradient_circuit`` - to the virtual parameter variables. A virtual parameter variable is added if a parameter - expression has more than one parameter. - coeff_map (dict): A dictionary maps the parameters of ``gradient_circuit`` to their coefficients - used to calculate gradients. - """ + """Stores gradient circuit data for the parameter shift method""" circuit: QuantumCircuit + """The original quantum circuit""" gradient_circuit: QuantumCircuit + """An internal quantum circuit used to calculate the gradient""" gradient_parameter_map: dict[Parameter, Parameter] + """A dictionary maps the parameters of ``circuit`` to the parameters of ``gradient_circuit``""" gradient_virtual_parameter_map: dict[Parameter, Parameter] + """A dictionary maps the parameters of ``gradient_circuit`` to the virtual parameter variables""" coeff_map: dict[Parameter, float | ParameterExpression] + """A dictionary maps the parameters of ``gradient_circuit`` to their coefficients""" def _make_param_shift_gradient_circuit_data( @@ -278,16 +271,12 @@ def _make_param_shift_parameter_values( @dataclass class LinearCombGradientCircuit: - """Gradient circuit for the linear combination of unitaries method. - - Args: - gradient_circuit (QuantumCircuit): A gradient circuit for the linear combination - of unitaries method. - coeff (float | ParameterExpression): A coefficient corresponds to the gradient circuit. - """ + """Gradient circuit for the linear combination of unitaries method.""" gradient_circuit: QuantumCircuit + """A gradient circuit for the linear combination of unitaries method.""" coeff: float | ParameterExpression + """A coefficient corresponds to the gradient circuit.""" def _make_lin_comb_gradient_circuit( From 9242360ef89e5e445baab5b6f17248cebdf56694 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 11:46:40 +0900 Subject: [PATCH 32/37] lint --- test/python/algorithms/test_sampler_gradient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index 86edd5c33abc..c3f759ba8abf 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -15,6 +15,7 @@ import unittest from test import combine +from typing import List import numpy as np from ddt import ddt @@ -510,7 +511,8 @@ def test_gradient_random_parameters(self, grad): array2 = _quasi2array(res2, num_qubits) np.testing.assert_allclose(array1, array2, rtol=1e-4) -def _quasi2array(quasis: list[QuasiDistribution], num_qubits: int) -> np.ndarray: + +def _quasi2array(quasis: List[QuasiDistribution], num_qubits: int) -> np.ndarray: ret = np.zeros((len(quasis), 2**num_qubits)) for i, quasi in enumerate(quasis): ret[i, list(quasi.keys())] = list(quasi.values()) From f8a45b0f4cf36bf8ca567708af2013f92ffcb164 Mon Sep 17 00:00:00 2001 From: a-matsuo <47442626+a-matsuo@users.noreply.github.com> Date: Fri, 2 Sep 2022 15:37:41 +0900 Subject: [PATCH 33/37] Update qiskit/algorithms/gradients/lin_comb_estimator_gradient.py Co-authored-by: Julien Gacon --- qiskit/algorithms/gradients/lin_comb_estimator_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 7924c3d8f761..968e39a04b06 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -70,7 +70,7 @@ def _run( circuits, observables, parameter_values, parameters ): # Make the observable as observable as :class:`~qiskit.quantum_info.SparsePauliOp`. - observables = init_observable(observable) + observable = init_observable(observable) # a set of parameters to be differentiated if parameters_ is None: param_set = set(circuit.parameters) From c17b53df487b8af5991b7bbaa65bd06a0fe96cb3 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 16:48:31 +0900 Subject: [PATCH 34/37] add operator tests Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../algorithms/test_estimator_gradient.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 904355f6524d..33b09118dbb2 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -30,7 +30,7 @@ from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate from qiskit.primitives import Estimator, Sampler -from qiskit.quantum_info import SparsePauliOp +from qiskit.quantum_info import SparsePauliOp, Operator from qiskit.quantum_info.random import random_pauli_list from qiskit.test import QiskitTestCase @@ -39,6 +39,33 @@ class TestEstimatorGradient(QiskitTestCase): """Test Estimator Gradient""" + @combine( + grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] + ) + def test_gradient_operators(self, grad): + """Test the estimator gradient for different operators""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + if grad is FiniteDiffEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + correct_result = -1 / np.sqrt(2) + param = [np.pi / 4] + value = gradient.run([qc], [op], [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + op = SparsePauliOp.from_list([("Z", 1)]) + value = gradient.run([qc], [op], [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + op = Operator.from_label('Z') + value = gradient.run([qc], [op], [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + @combine( grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] ) From 595742263bdb9883a5f33f8653ae56b946ff0ac9 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 17:00:56 +0900 Subject: [PATCH 35/37] consistent name --- qiskit/algorithms/gradients/spsa_sampler_gradient.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index c1836048832a..cfb352b26fa3 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -105,17 +105,17 @@ def _run( grad_dists[j, list(dist_plus.keys())] += list(dist_plus.values()) grad_dists[j, list(dist_minus.keys())] -= list(dist_minus.values()) grad_dists /= 2 * self._epsilon - gradient_ = [] + gradient = [] indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] for j in range(circuits[i].num_parameters): if not j in indices: continue - grad = np.mean( - np.array([delta * dist for dist, delta in zip(grad_dists, offsets[i][:, j])]), + gradient_ = np.mean( + np.array([offset * grad for grad, offset in zip(grad_dists, offsets[i][:, j])]), axis=0, ) - gradient_.append(dict(enumerate(grad))) - gradients.append(gradient_) + gradient.append(dict(enumerate(gradient_))) + gradients.append(gradient) # TODO: include primitive's run_options as well return SamplerGradientResult( From dd960a0fe4b2973d08a2626b46ac11ec15c9954a Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Fri, 2 Sep 2022 17:40:37 +0900 Subject: [PATCH 36/37] rewrite spsa Co-authored-by: Ikko Hamamura Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- .../gradients/spsa_estimator_gradient.py | 12 +++++++----- .../gradients/spsa_sampler_gradient.py | 18 ++++++++++-------- .../algorithms/test_estimator_gradient.py | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 85919e51dee1..86e8e96f8157 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -104,15 +104,17 @@ def _run( # combine the results if any(job.status() is not JobStatus.DONE for job in jobs): raise QiskitError("The gradient job was not completed successfully. ") + results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) - # take the average of the spsa gradients - gradient = np.average( - np.array([grad / offset for grad, offset in zip(gradient_, offsets[i])]), axis=0 - ) + diffs = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) + # calculate the gradient for each batch. Note that (``diff`` / ``offset``) is the gradient + # since ``offset`` is a perturbation vector of 1s and -1s. + batch_gradients = np.array([diff / offset for diff, offset in zip(diffs, offsets[i])]) + # take the average of the batch gradients + gradient = np.mean(batch_gradients, axis=0) indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] gradients.append(gradient[indices]) diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index cfb352b26fa3..5a1ed9a63604 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -98,23 +98,25 @@ def _run( results = [job.result() for job in jobs] gradients = [] for i, result in enumerate(results): - grad_dists = np.zeros((self._batch_size, 2 ** circuits[i].num_qubits)) + dist_diffs = np.zeros((self._batch_size, 2 ** circuits[i].num_qubits)) for j, (dist_plus, dist_minus) in enumerate( zip(result.quasi_dists[: self._batch_size], result.quasi_dists[self._batch_size :]) ): - grad_dists[j, list(dist_plus.keys())] += list(dist_plus.values()) - grad_dists[j, list(dist_minus.keys())] -= list(dist_minus.values()) - grad_dists /= 2 * self._epsilon + dist_diffs[j, list(dist_plus.keys())] += list(dist_plus.values()) + dist_diffs[j, list(dist_minus.keys())] -= list(dist_minus.values()) + dist_diffs /= 2 * self._epsilon gradient = [] indices = [circuits[i].parameters.data.index(p) for p in metadata_[i]["parameters"]] for j in range(circuits[i].num_parameters): if not j in indices: continue - gradient_ = np.mean( - np.array([offset * grad for grad, offset in zip(grad_dists, offsets[i][:, j])]), - axis=0, + # the gradient for jth parameter is the average of the gradients of the jth parameter + # for each batch. + batch_gradients = np.array( + [offset * dist_diff for dist_diff, offset in zip(dist_diffs, offsets[i][:, j])] ) - gradient.append(dict(enumerate(gradient_))) + gradient_j = np.mean(batch_gradients, axis=0) + gradient.append(dict(enumerate(gradient_j))) gradients.append(gradient) # TODO: include primitive's run_options as well diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 33b09118dbb2..49934a6f25e4 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -62,7 +62,7 @@ def test_gradient_operators(self, grad): op = SparsePauliOp.from_list([("Z", 1)]) value = gradient.run([qc], [op], [param]).result().gradients[0] self.assertAlmostEqual(value[0], correct_result, 3) - op = Operator.from_label('Z') + op = Operator.from_label("Z") value = gradient.run([qc], [op], [param]).result().gradients[0] self.assertAlmostEqual(value[0], correct_result, 3) From c16672f2abda9c96ddf158d5bad43d4263fa6a25 Mon Sep 17 00:00:00 2001 From: a-matsuo Date: Mon, 5 Sep 2022 19:08:39 +0900 Subject: [PATCH 37/37] use algorithm job --- .../gradients/base_estimator_gradient.py | 13 +++---------- .../algorithms/gradients/base_sampler_gradient.py | 15 ++++----------- .../gradients/finite_diff_estimator_gradient.py | 10 +++++----- .../gradients/finite_diff_sampler_gradient.py | 11 ++++++----- .../gradients/lin_comb_estimator_gradient.py | 11 ++++++----- .../gradients/lin_comb_sampler_gradient.py | 11 ++++++----- .../gradients/param_shift_estimator_gradient.py | 14 +++++++------- .../gradients/param_shift_sampler_gradient.py | 11 ++++++----- .../gradients/spsa_estimator_gradient.py | 11 ++++++----- .../algorithms/gradients/spsa_sampler_gradient.py | 13 +++++++------ ...adients-with-primitives-561cf9cf75a7ccb8.yaml} | 5 +++-- test/python/algorithms/test_estimator_gradient.py | 6 +----- test/python/algorithms/test_sampler_gradient.py | 6 +----- 13 files changed, 61 insertions(+), 76 deletions(-) rename releasenotes/notes/{gradients-primitives-561cf9cf75a7ccb8.yaml => add-gradients-with-primitives-561cf9cf75a7ccb8.yaml} (65%) diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py index 7b66225b7474..7a2819c8b566 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base_estimator_gradient.py @@ -23,7 +23,7 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator -from qiskit.primitives.primitive_job import PrimitiveJob +from qiskit.algorithms import AlgorithmJob from qiskit.quantum_info.operators.base_operator import BaseOperator from .estimator_gradient_result import EstimatorGradientResult @@ -43,14 +43,7 @@ def __init__( run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in ``run`` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. - - Raises: - ValueError: If the estimator is not an instance of ``BaseEstimator``. """ - if not isinstance(estimator, BaseEstimator): - raise ValueError( - f"The estimator should be an instance of BaseEstimator, but got {type(estimator)}" - ) self._estimator: BaseEstimator = estimator self._default_run_options = run_options @@ -61,7 +54,7 @@ def run( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, - ) -> PrimitiveJob: + ) -> AlgorithmJob: """Run the job of the estimator gradient on the given circuits. Args: @@ -95,7 +88,7 @@ def run( run_opts = copy(self._default_run_options) run_opts.update(run_options) - job = PrimitiveJob( + job = AlgorithmJob( self._run, circuits, observables, parameter_values, parameters, **run_opts ) job.submit() diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py index f5e5d3e862bb..f6cc4611096e 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base_sampler_gradient.py @@ -22,7 +22,7 @@ from qiskit.circuit import QuantumCircuit, Parameter from qiskit.primitives import BaseSampler -from qiskit.primitives.primitive_job import PrimitiveJob +from qiskit.algorithms import AlgorithmJob from .sampler_gradient_result import SamplerGradientResult @@ -36,14 +36,7 @@ def __init__(self, sampler: BaseSampler, **run_options): run_options: Backend runtime options used for circuit execution. The order of priority is: run_options in `run` method > gradient's default run_options > primitive's default setting. Higher priority setting overrides lower priority setting. - - Raises: - ValueError: If the sampler is not an instance of ``BaseSampler``. """ - if not isinstance(sampler, BaseSampler): - raise ValueError( - f"The sampler should be an instance of BaseSampler, but got {type(sampler)}" - ) self._sampler: BaseSampler = sampler self._default_run_options = run_options @@ -53,7 +46,7 @@ def run( parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter] | None] | None = None, **run_options, - ) -> PrimitiveJob: + ) -> AlgorithmJob: """Run the job of the sampler gradient on the given circuits. Args: @@ -68,7 +61,7 @@ def run( setting. Higher priority setting overrides lower priority setting. Returns: - Primitive job contains the gradients of the sampling probability. The i-th result + The job object of the gradients of the sampling probability. The i-th result corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th quasi-probability distribution in the i-th result corresponds to the gradients of the sampling probability for the j-th parameter in ``circuits[i]``. @@ -85,7 +78,7 @@ def run( # run_options in `run` method > gradient's default run_options > primitive's default run_options. run_opts = copy(self._default_run_options) run_opts.update(run_options) - job = PrimitiveJob(self._run, circuits, parameter_values, parameters, **run_opts) + job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **run_opts) job.submit() return job diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index b4e2e65041da..ecb72288e4bc 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -18,11 +18,10 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator -from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -83,10 +82,11 @@ def _run( jobs.append(job) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc - results = [job.result() for job in jobs] gradients = [] for result in results: n = len(result.values) // 2 # is always a multiple of 2 diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index b25511ac2efe..9ad28cfc8a09 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -18,10 +18,9 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler -from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -76,9 +75,11 @@ def _run( jobs.append(job) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + gradients = [] for i, result in enumerate(results): n = len(result.quasi_dists) // 2 diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py index 968e39a04b06..72d4b9c59cd5 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py @@ -19,12 +19,11 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.primitives.utils import init_observable -from qiskit.providers import JobStatus from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -118,9 +117,11 @@ def _run( coeffs_all.append(coeffs) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc + gradients = [] for i, result in enumerate(results): gradient_ = np.zeros(len(metadata_[i]["parameters"])) diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py index 183779eebcd8..56a7953d05c8 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py @@ -19,10 +19,9 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler -from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -103,9 +102,11 @@ def _run( coeffs_all.append(coeffs) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + gradients = [] for i, result in enumerate(results): n = 2 ** circuits[i].num_qubits diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py index 09e708b1fcc4..15eebf7f9987 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_estimator_gradient.py @@ -19,17 +19,15 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator -from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult - -from .utils import _param_shift_preprocessing, _make_param_shift_parameter_values +from .utils import _make_param_shift_parameter_values, _param_shift_preprocessing class ParamShiftEstimatorGradient(BaseEstimatorGradient): @@ -98,9 +96,11 @@ def _run( coeffs_all.append(coeffs) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc + gradients = [] for i, result in enumerate(results): n = len(result.values) // 2 # is always a multiple of 2 diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py index e42256bb71db..6265fc6f61b2 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift_sampler_gradient.py @@ -19,10 +19,9 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler -from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -92,9 +91,11 @@ def _run( coeffs_all.append(coeffs) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + gradients = [] for i, result in enumerate(results): n = len(result.quasi_dists) // 2 diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa_estimator_gradient.py index 86e8e96f8157..37828c346697 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa_estimator_gradient.py @@ -18,11 +18,10 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator -from qiskit.providers import JobStatus from qiskit.quantum_info.operators.base_operator import BaseOperator from .base_estimator_gradient import BaseEstimatorGradient @@ -60,7 +59,7 @@ def __init__( raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._batch_size = batch_size - self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() + self._seed = np.random.default_rng(seed) super().__init__(estimator, **run_options) @@ -102,8 +101,10 @@ def _run( jobs.append(job) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc results = [job.result() for job in jobs] gradients = [] diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa_sampler_gradient.py index 5a1ed9a63604..578872db2554 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa_sampler_gradient.py @@ -18,10 +18,9 @@ import numpy as np +from qiskit.algorithms import AlgorithmError from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError from qiskit.primitives import BaseSampler -from qiskit.providers import JobStatus from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult @@ -58,7 +57,7 @@ def __init__( raise ValueError(f"epsilon ({epsilon}) should be positive.") self._batch_size = batch_size self._epsilon = epsilon - self._seed = np.random.default_rng(seed) if seed else np.random.default_rng() + self._seed = np.random.default_rng(seed) super().__init__(sampler, **run_options) @@ -93,9 +92,11 @@ def _run( jobs.append(job) # combine the results - if any(job.status() is not JobStatus.DONE for job in jobs): - raise QiskitError("The gradient job was not completed successfully. ") - results = [job.result() for job in jobs] + try: + results = [job.result() for job in jobs] + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + gradients = [] for i, result in enumerate(results): dist_diffs = np.zeros((self._batch_size, 2 ** circuits[i].num_qubits)) diff --git a/releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml b/releasenotes/notes/add-gradients-with-primitives-561cf9cf75a7ccb8.yaml similarity index 65% rename from releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml rename to releasenotes/notes/add-gradients-with-primitives-561cf9cf75a7ccb8.yaml index fc304ae64e41..93bd3b5d155a 100644 --- a/releasenotes/notes/gradients-primitives-561cf9cf75a7ccb8.yaml +++ b/releasenotes/notes/add-gradients-with-primitives-561cf9cf75a7ccb8.yaml @@ -1,7 +1,7 @@ --- features: - | - New gradient classes using the primitives has been added. They internally + New gradient Algorithms using the primitives have been added. They internally use the primitives to calculate the gradients. There are 3 types of gradient classes (Finite Difference, Parameter Shift, and Linear Combination of Unitary) for a sampler and estimator. @@ -11,4 +11,5 @@ features: estimator = Estimator(...) gradient = ParamShiftEstimatorGradient(estimator) - results = gradient.evaluate(circuits, observables, parameters) + job = gradient.run(circuits, observables, parameters) + gradients = job.result().gradients diff --git a/test/python/algorithms/test_estimator_gradient.py b/test/python/algorithms/test_estimator_gradient.py index 49934a6f25e4..a139350e27b5 100644 --- a/test/python/algorithms/test_estimator_gradient.py +++ b/test/python/algorithms/test_estimator_gradient.py @@ -29,7 +29,7 @@ from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate -from qiskit.primitives import Estimator, Sampler +from qiskit.primitives import Estimator from qiskit.quantum_info import SparsePauliOp, Operator from qiskit.quantum_info.random import random_pauli_list from qiskit.test import QiskitTestCase @@ -283,14 +283,10 @@ def test_gradient_validation(self, grad): qc.rx(a, 0) if grad is FiniteDiffEstimatorGradient: gradient = grad(estimator, epsilon=1e-6) - with self.assertRaises(ValueError): - _ = grad(Sampler(), epsilon=1e-6) with self.assertRaises(ValueError): _ = grad(estimator, epsilon=-0.1) else: gradient = grad(estimator) - with self.assertRaises(ValueError): - _ = grad(Sampler()) param_list = [[np.pi / 4], [np.pi / 2]] op = SparsePauliOp.from_list([("Z", 1)]) with self.assertRaises(ValueError): diff --git a/test/python/algorithms/test_sampler_gradient.py b/test/python/algorithms/test_sampler_gradient.py index c3f759ba8abf..51e0246aee6c 100644 --- a/test/python/algorithms/test_sampler_gradient.py +++ b/test/python/algorithms/test_sampler_gradient.py @@ -30,7 +30,7 @@ from qiskit.circuit import Parameter from qiskit.circuit.library import EfficientSU2, RealAmplitudes from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate -from qiskit.primitives import Estimator, Sampler +from qiskit.primitives import Sampler from qiskit.result import QuasiDistribution from qiskit.test import QiskitTestCase @@ -379,14 +379,10 @@ def test_gradient_validation(self, grad): qc.measure_all() if grad is FiniteDiffSamplerGradient: gradient = grad(sampler, epsilon=1e-6) - with self.assertRaises(ValueError): - _ = grad(Estimator(), epsilon=1e-6) with self.assertRaises(ValueError): _ = grad(sampler, epsilon=-0.1) else: gradient = grad(sampler) - with self.assertRaises(ValueError): - _ = grad(Estimator()) param_list = [[np.pi / 4], [np.pi / 2]] with self.assertRaises(ValueError): gradient.run([qc], param_list)