From 829781ccaafbc471eb87ce1697e1ad4ffb01f74d Mon Sep 17 00:00:00 2001 From: ikkoham Date: Wed, 9 Nov 2022 17:02:41 +0900 Subject: [PATCH 1/8] implement gradient methods --- .../finite_diff_estimator_gradient.py | 62 +++++++++++--- .../gradients/finite_diff_sampler_gradient.py | 73 ++++++++++++++--- .../gradient-methods-b2ec34916b83c17b.yaml | 16 ++++ .../gradients/test_estimator_gradient.py | 80 ++++++------------- .../gradients/test_sampler_gradient.py | 57 +++++-------- 5 files changed, 175 insertions(+), 113 deletions(-) create mode 100644 releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 5ab14150b8a6..c71a1951e017 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 +import sys from typing import Sequence import numpy as np @@ -28,17 +29,32 @@ from .base_estimator_gradient import BaseEstimatorGradient from .estimator_gradient_result import EstimatorGradientResult +if sys.version_info >= (3, 8): + # pylint: disable=no-name-in-module, ungrouped-imports + from typing import Literal +else: + from typing_extensions import Literal + class FiniteDiffEstimatorGradient(BaseEstimatorGradient): """ Compute the gradients of the expectation values by finite difference method. """ - def __init__(self, estimator: BaseEstimator, epsilon: float, options: Options | None = None): + def __init__( + self, + estimator: BaseEstimator, + epsilon: float, + method: Literal["central", "forward", "backward"] = "central", + options: Options | None = None, + ): """ Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. + method: The calculation method of the gradient. "central" calculates + :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" :math:`\frac{f(x+e) - f(x)}{e}`, + and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > gradient's default options > primitive's default setting. @@ -46,11 +62,17 @@ def __init__(self, estimator: BaseEstimator, epsilon: float, options: Options | Raises: ValueError: If ``epsilon`` is not positive. + TypeError: If ``method`` is invalid. """ if epsilon <= 0: raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon self._base_parameter_values_dict = {} + if method not in ("central", "forward", "backward"): + raise TypeError( + f"The argument method should be central, forward, or backward: {method} is given." + ) + self._method = method super().__init__(estimator, options) def _run( @@ -74,12 +96,25 @@ def _run( 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._estimator.run( - [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **options - ) + if self._method == "central": + plus = parameter_values_ + self._epsilon * offset / 2 + minus = parameter_values_ - self._epsilon * offset / 2 + n = 2 * len(indices) + job = self._estimator.run( + [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **options + ) + elif self._method == "forward": + plus = parameter_values_ + self._epsilon * offset + n = len(indices) + 1 + job = self._estimator.run( + [circuit] * n, [observable] * n, [parameter_values_] + plus.tolist(), **options + ) + elif self._method == "backward": + minus = parameter_values_ - self._epsilon * offset + n = len(indices) + 1 + job = self._estimator.run( + [circuit] * n, [observable] * n, [parameter_values_] + minus.tolist(), **options + ) jobs.append(job) # combine the results @@ -90,8 +125,15 @@ def _run( 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) - gradients.append(gradient_) + if self._method == "central": + n = len(result.values) // 2 # is always a multiple of 2 + gradient_ = (result.values[:n] - result.values[n:]) / self._epsilon + gradients.append(gradient_) + elif self._method == "forward": + gradient_ = (result.values[1:] - result.values[0]) / self._epsilon + gradients.append(gradient_) + elif self._method == "backward": + gradient_ = (result.values[0] - result.values[1:]) / self._epsilon + gradients.append(gradient_) opt = self._get_local_options(options) return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index c5c9fdc00806..4173850b131e 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 +import sys from typing import Sequence import numpy as np @@ -26,6 +27,12 @@ from .base_sampler_gradient import BaseSamplerGradient from .sampler_gradient_result import SamplerGradientResult +if sys.version_info >= (3, 8): + # pylint: disable=no-name-in-module, ungrouped-imports + from typing import Literal +else: + from typing_extensions import Literal + class FiniteDiffSamplerGradient(BaseSamplerGradient): """Compute the gradients of the sampling probability by finite difference method.""" @@ -34,12 +41,16 @@ def __init__( self, sampler: BaseSampler, epsilon: float, + method: Literal["central", "forward", "backward"] = "central", options: Options | None = None, ): """ Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. + method: The calculation method of the gradient. "central" calculates + :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" :math:`\frac{f(x+e) - f(x)}{e}`, + and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > gradient's default options > primitive's default setting. @@ -47,10 +58,16 @@ def __init__( Raises: ValueError: If ``epsilon`` is not positive. + TypeError: If ``method`` is invalid. """ if epsilon <= 0: raise ValueError(f"epsilon ({epsilon}) should be positive.") self._epsilon = epsilon + if method not in ("central", "forward", "backward"): + raise TypeError( + f"The argument method should be central, forward, or backward: {method} is given." + ) + self._method = method super().__init__(sampler, options) def _run( @@ -70,10 +87,23 @@ def _run( indices = [circuit.parameters.data.index(p) for p in parameters_] 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(), **options) + if self._method == "central": + plus = parameter_values_ + self._epsilon * offset / 2 + minus = parameter_values_ - self._epsilon * offset / 2 + n = 2 * len(indices) + job = self._sampler.run([circuit] * n, plus.tolist() + minus.tolist(), **options) + elif self._method == "forward": + plus = parameter_values_ + self._epsilon * offset + n = len(indices) + 1 + job = self._sampler.run( + [circuit] * n, [parameter_values_] + plus.tolist(), **options + ) + elif self._method == "backward": + minus = parameter_values_ - self._epsilon * offset + n = len(indices) + 1 + job = self._sampler.run( + [circuit] * n, [parameter_values_] + minus.tolist(), **options + ) jobs.append(job) # combine the results @@ -84,14 +114,33 @@ def _run( gradients = [] for i, result in enumerate(results): - n = len(result.quasi_dists) // 2 - 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(dict(enumerate(grad_dist))) + if self._method == "central": + n = len(result.quasi_dists) // 2 + 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 /= self._epsilon + gradient_.append(dict(enumerate(grad_dist))) + elif self._method == "forward": + gradient_ = [] + dist_zero = result.quasi_dists[0] + for dist_plus in result.quasi_dists[1:]: + grad_dist = np.zeros(2 ** circuits[i].num_qubits) + grad_dist[list(dist_plus.keys())] += list(dist_plus.values()) + grad_dist[list(dist_zero.keys())] -= list(dist_zero.values()) + grad_dist /= self._epsilon + gradient_.append(dict(enumerate(grad_dist))) + elif self._method == "backward": + gradient_ = [] + dist_zero = result.quasi_dists[0] + for dist_minus in result.quasi_dists[1:]: + grad_dist = np.zeros(2 ** circuits[i].num_qubits) + grad_dist[list(dist_zero.keys())] += list(dist_zero.values()) + grad_dist[list(dist_minus.keys())] -= list(dist_minus.values()) + grad_dist /= self._epsilon + gradient_.append(dict(enumerate(grad_dist))) gradients.append(gradient_) opt = self._get_local_options(options) diff --git a/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml b/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml new file mode 100644 index 000000000000..28e7e0421462 --- /dev/null +++ b/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + :class:`.FiniteDiffEstimatorGradient` and :class:`FiniteDiffSamplerGradient` + have new argument method. + There are three methods, "central", "forward", and "backward". + This option changes the gradient calculation methods. + "central" calculates :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" + :math:`\frac{f(x+e) - f(x)}{e}`, and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where + :math:`e` is the offset epsilon. +fixes: + - | + :class:`.FiniteDiffEstimatorGradient` and :class:`FiniteDiffSamplerGradient` + calculated gradients by :math:`\frac{f(x+e)-f(x-e)}{2e}`. + Fixed to correct formula as central difference. That is, it is calculated as + :math:`\frac{f(x+e/2)-f(x-e/2)}{e}` diff --git a/test/python/algorithms/gradients/test_estimator_gradient.py b/test/python/algorithms/gradients/test_estimator_gradient.py index 5172e202628b..fa69e41d0ef8 100644 --- a/test/python/algorithms/gradients/test_estimator_gradient.py +++ b/test/python/algorithms/gradients/test_estimator_gradient.py @@ -34,14 +34,20 @@ from qiskit.quantum_info.random import random_pauli_list from qiskit.test import QiskitTestCase +gradient_factories = [ + lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="central"), + lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="forward"), + lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="backward"), + ParamShiftEstimatorGradient, + LinCombEstimatorGradient, +] + @ddt class TestEstimatorGradient(QiskitTestCase): """Test Estimator Gradient""" - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_operators(self, grad): """Test the estimator gradient for different operators""" estimator = Estimator() @@ -50,10 +56,7 @@ def test_gradient_operators(self, grad): qc.h(0) qc.p(a, 0) qc.h(0) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + gradient = grad(estimator) op = SparsePauliOp.from_list([("Z", 1)]) correct_result = -1 / np.sqrt(2) param = [np.pi / 4] @@ -66,9 +69,7 @@ def test_gradient_operators(self, grad): value = gradient.run([qc], [op], [param]).result().gradients[0] self.assertAlmostEqual(value[0], correct_result, 3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_p(self, grad): """Test the estimator gradient for p""" estimator = Estimator() @@ -77,10 +78,7 @@ def test_gradient_p(self, grad): qc.h(0) qc.p(a, 0) qc.h(0) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + 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]] @@ -89,9 +87,7 @@ def test_gradient_p(self, grad): for j, value in enumerate(gradients): self.assertAlmostEqual(value, correct_results[i][j], 3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_u(self, grad): """Test the estimator gradient for u""" estimator = Estimator() @@ -102,10 +98,7 @@ def test_gradient_u(self, grad): qc.h(0) qc.u(a, b, c, 0) qc.h(0) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + 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]] @@ -115,18 +108,13 @@ def test_gradient_u(self, grad): for j, value in enumerate(gradients): self.assertAlmostEqual(value, correct_results[i][j], 3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) 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)]) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + gradient = grad(estimator) param_list = [ [np.pi / 4 for param in qc.parameters], [np.pi / 2 for param in qc.parameters], @@ -148,9 +136,7 @@ def test_gradient_efficient_su2(self, grad): gradients = gradient.run([qc], [op], [param]).result().gradients[0] np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient], - ) + @combine(grad=gradient_factories) def test_gradient_2qubit_gate(self, grad): """Test the estimator gradient for 2 qubit gates""" estimator = Estimator() @@ -164,10 +150,7 @@ def test_gradient_2qubit_gate(self, grad): for i, param in enumerate(param_list): a = Parameter("a") qc = QuantumCircuit(2) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + gradient = grad(estimator) if gate is RZZGate: qc.h([0, 1]) @@ -178,9 +161,7 @@ def test_gradient_2qubit_gate(self, grad): gradients = gradient.run([qc], [op], [param]).result().gradients[0] np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_parameter_coefficient(self, grad): """Test the estimator gradient for parameter variables with coefficients""" estimator = Estimator() @@ -190,10 +171,7 @@ 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) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + 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], @@ -204,9 +182,7 @@ def test_gradient_parameter_coefficient(self, grad): gradients = gradient.run([qc], [op], [param]).result().gradients[0] np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_parameters(self, grad): """Test the estimator gradient for parameters""" estimator = Estimator() @@ -215,10 +191,7 @@ def test_gradient_parameters(self, grad): qc = QuantumCircuit(1) qc.rx(a, 0) qc.rx(b, 0) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + gradient = grad(estimator) param_list = [[np.pi / 4, np.pi / 2]] correct_results = [ [-0.70710678], @@ -228,9 +201,7 @@ def test_gradient_parameters(self, grad): gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0] np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) - @combine( - grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient] - ) + @combine(grad=gradient_factories) def test_gradient_multi_arguments(self, grad): """Test the estimator gradient for multiple arguments""" estimator = Estimator() @@ -240,10 +211,7 @@ def test_gradient_multi_arguments(self, grad): qc.rx(a, 0) qc2 = QuantumCircuit(1) qc2.rx(b, 0) - if grad is FiniteDiffEstimatorGradient: - gradient = grad(estimator, epsilon=1e-6) - else: - gradient = grad(estimator) + gradient = grad(estimator) param_list = [[np.pi / 4], [np.pi / 2]] correct_results = [ [-0.70710678], diff --git a/test/python/algorithms/gradients/test_sampler_gradient.py b/test/python/algorithms/gradients/test_sampler_gradient.py index bfd4453c96dd..6bdc120cb07b 100644 --- a/test/python/algorithms/gradients/test_sampler_gradient.py +++ b/test/python/algorithms/gradients/test_sampler_gradient.py @@ -34,12 +34,20 @@ from qiskit.result import QuasiDistribution from qiskit.test import QiskitTestCase +gradient_factories = [ + lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="central"), + lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="forward"), + lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="backward"), + ParamShiftSamplerGradient, + LinCombSamplerGradient, +] + @ddt class TestSamplerGradient(QiskitTestCase): """Test Sampler Gradient""" - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_p(self, grad): """Test the sampler gradient for p""" sampler = Sampler() @@ -49,10 +57,7 @@ def test_gradient_p(self, grad): qc.p(a, 0) qc.h(0) qc.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + 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)}], @@ -65,7 +70,7 @@ def test_gradient_p(self, grad): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_u(self, grad): """Test the sampler gradient for u""" sampler = Sampler() @@ -77,10 +82,7 @@ def test_gradient_u(self, grad): qc.u(a, b, c, 0) qc.h(0) qc.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + 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}], @@ -92,16 +94,13 @@ def test_gradient_u(self, grad): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_efficient_su2(self, grad): """Test the sampler gradient for EfficientSU2""" sampler = Sampler() qc = EfficientSU2(2, reps=1) qc.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + gradient = grad(sampler) param_list = [ [np.pi / 4 for param in qc.parameters], [np.pi / 2 for param in qc.parameters], @@ -189,7 +188,7 @@ def test_gradient_efficient_su2(self, grad): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_2qubit_gate(self, grad): """Test the sampler gradient for 2 qubit gates""" sampler = Sampler() @@ -211,16 +210,13 @@ def test_gradient_2qubit_gate(self, grad): qc = QuantumCircuit(2) qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) qc.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + gradient = grad(sampler) 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]) + @combine(grad=gradient_factories) def test_gradient_parameter_coefficient(self, grad): """Test the sampler gradient for parameter variables with coefficients""" sampler = Sampler() @@ -231,10 +227,7 @@ 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() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + gradient = grad(sampler) param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] correct_results = [ [ @@ -297,7 +290,7 @@ def test_gradient_parameter_coefficient(self, grad): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 2) - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_parameters(self, grad): """Test the sampler gradient for parameters""" sampler = Sampler() @@ -307,10 +300,7 @@ def test_gradient_parameters(self, grad): qc.rx(a, 0) qc.rz(b, 0) qc.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + 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)}], @@ -321,7 +311,7 @@ def test_gradient_parameters(self, grad): for k in quasi_dist: self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3) - @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient]) + @combine(grad=gradient_factories) def test_gradient_multi_arguments(self, grad): """Test the sampler gradient for multiple arguments""" sampler = Sampler() @@ -333,10 +323,7 @@ def test_gradient_multi_arguments(self, grad): qc2 = QuantumCircuit(1) qc2.rx(b, 0) qc2.measure_all() - if grad is FiniteDiffSamplerGradient: - gradient = grad(sampler, epsilon=1e-6) - else: - gradient = grad(sampler) + 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)}], From 89115548f4719b2ff609d748558aca3b3f099935 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Wed, 9 Nov 2022 18:46:43 +0900 Subject: [PATCH 2/8] fix docs --- .../gradients/finite_diff_estimator_gradient.py | 10 +++++++--- .../gradients/finite_diff_sampler_gradient.py | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index c71a1951e017..e175001db8fd 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -52,9 +52,13 @@ def __init__( Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. - method: The calculation method of the gradient. "central" calculates - :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" :math:`\frac{f(x+e) - f(x)}{e}`, - and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. + method: The computation method of the gradients. + + - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, + - ``\"forward\"`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` + + where :math:`e` is epsilon. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > gradient's default options > primitive's default setting. diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 4173850b131e..0106735d7029 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -48,9 +48,13 @@ def __init__( Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. - method: The calculation method of the gradient. "central" calculates - :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" :math:`\frac{f(x+e) - f(x)}{e}`, - and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. + method: The computation method of the gradients. + + - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, + - ``\"forward\"`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` + + where :math:`e` is epsilon. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > gradient's default options > primitive's default setting. From 842350fa479a4190ad5fd612c479c4b3d0ba44c9 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Wed, 9 Nov 2022 19:23:42 +0900 Subject: [PATCH 3/8] arg order/refactor --- .../gradients/finite_diff_estimator_gradient.py | 15 +++++++-------- .../gradients/finite_diff_sampler_gradient.py | 11 ++++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index e175001db8fd..a4b495376f34 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -45,13 +45,18 @@ def __init__( self, estimator: BaseEstimator, epsilon: float, - method: Literal["central", "forward", "backward"] = "central", options: Options | None = None, + *, + method: Literal["central", "forward", "backward"] = "central", ): """ Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting method: The computation method of the gradients. - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, @@ -59,10 +64,6 @@ def __init__( - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. @@ -132,12 +133,10 @@ def _run( if self._method == "central": n = len(result.values) // 2 # is always a multiple of 2 gradient_ = (result.values[:n] - result.values[n:]) / self._epsilon - gradients.append(gradient_) elif self._method == "forward": gradient_ = (result.values[1:] - result.values[0]) / self._epsilon - gradients.append(gradient_) elif self._method == "backward": gradient_ = (result.values[0] - result.values[1:]) / self._epsilon - gradients.append(gradient_) + gradients.append(gradient_) opt = self._get_local_options(options) return EstimatorGradientResult(gradients=gradients, metadata=metadata_, options=opt) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 0106735d7029..cd84e60f909b 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -41,13 +41,18 @@ def __init__( self, sampler: BaseSampler, epsilon: float, - method: Literal["central", "forward", "backward"] = "central", options: Options | None = None, + *, + method: Literal["central", "forward", "backward"] = "central", ): """ Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting method: The computation method of the gradients. - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, @@ -55,10 +60,6 @@ def __init__( - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. - options: Primitive backend runtime options used for circuit execution. - The order of priority is: options in ``run`` method > gradient's - default options > primitive's default setting. - Higher priority setting overrides lower priority setting Raises: ValueError: If ``epsilon`` is not positive. From 7e470815558cc4e42e5be85f8c3a6d9e537233a3 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Wed, 9 Nov 2022 20:14:51 +0900 Subject: [PATCH 4/8] fix docs --- .../algorithms/gradients/finite_diff_estimator_gradient.py | 6 +++--- qiskit/algorithms/gradients/finite_diff_sampler_gradient.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index a4b495376f34..23bce2559ded 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -59,9 +59,9 @@ def __init__( Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, - - ``\"forward\"`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` + - ``central`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index cd84e60f909b..809c7a46cdc4 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -55,9 +55,9 @@ def __init__( Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - ``\"central\"`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, - - ``\"forward\"`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``\"backward\"`` computes :math:`\frac{f(x)-f(x-e)}{e}` + - ``central`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. From a5fa8422a1766c15832ad391f8d0dbbd740066c6 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Thu, 10 Nov 2022 11:04:50 +0900 Subject: [PATCH 5/8] revert central difference --- .../gradients/finite_diff_estimator_gradient.py | 12 ++++++------ .../gradients/finite_diff_sampler_gradient.py | 14 ++++++-------- .../notes/gradient-methods-b2ec34916b83c17b.yaml | 8 +------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 23bce2559ded..dd12f8cba0b8 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -59,9 +59,9 @@ def __init__( Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - ``central`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` + - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. @@ -102,8 +102,8 @@ def _run( offset = np.identity(circuit.num_parameters)[indices, :] if self._method == "central": - plus = parameter_values_ + self._epsilon * offset / 2 - minus = parameter_values_ - self._epsilon * offset / 2 + plus = parameter_values_ + self._epsilon * offset + minus = parameter_values_ - self._epsilon * offset n = 2 * len(indices) job = self._estimator.run( [circuit] * n, [observable] * n, plus.tolist() + minus.tolist(), **options @@ -132,7 +132,7 @@ def _run( for result in results: if self._method == "central": n = len(result.values) // 2 # is always a multiple of 2 - gradient_ = (result.values[:n] - result.values[n:]) / self._epsilon + gradient_ = (result.values[:n] - result.values[n:]) / (2 * self._epsilon) elif self._method == "forward": gradient_ = (result.values[1:] - result.values[0]) / self._epsilon elif self._method == "backward": diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 809c7a46cdc4..54077a53c237 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -54,11 +54,9 @@ def __init__( default options > primitive's default setting. Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - - ``central`` computes :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` - + - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. Raises: @@ -93,8 +91,8 @@ def _run( metadata_.append({"parameters": [circuit.parameters[idx] for idx in indices]}) offset = np.identity(circuit.num_parameters)[indices, :] if self._method == "central": - plus = parameter_values_ + self._epsilon * offset / 2 - minus = parameter_values_ - self._epsilon * offset / 2 + 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(), **options) elif self._method == "forward": @@ -126,7 +124,7 @@ 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 /= self._epsilon + grad_dist /= 2 * self._epsilon gradient_.append(dict(enumerate(grad_dist))) elif self._method == "forward": gradient_ = [] diff --git a/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml b/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml index 28e7e0421462..fedc7c41d806 100644 --- a/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml +++ b/releasenotes/notes/gradient-methods-b2ec34916b83c17b.yaml @@ -5,12 +5,6 @@ features: have new argument method. There are three methods, "central", "forward", and "backward". This option changes the gradient calculation methods. - "central" calculates :math:`\frac{f(x+e/2)-f(x-e/2)}{e}`, "forward" + "central" calculates :math:`\frac{f(x+e)-f(x-e)}{2e}`, "forward" :math:`\frac{f(x+e) - f(x)}{e}`, and "backward" :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is the offset epsilon. -fixes: - - | - :class:`.FiniteDiffEstimatorGradient` and :class:`FiniteDiffSamplerGradient` - calculated gradients by :math:`\frac{f(x+e)-f(x-e)}{2e}`. - Fixed to correct formula as central difference. That is, it is calculated as - :math:`\frac{f(x+e/2)-f(x-e/2)}{e}` From 9394542e0205cde6635ff8bbfedd863f8762250c Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 11 Nov 2022 16:49:11 +0100 Subject: [PATCH 6/8] Update qiskit/algorithms/gradients/finite_diff_estimator_gradient.py --- .../algorithms/gradients/finite_diff_estimator_gradient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index dd12f8cba0b8..0b63aff5b7b3 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -59,9 +59,9 @@ def __init__( Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` + - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. From 6520ef131182756ee6261a32e5bf97e0fe94d656 Mon Sep 17 00:00:00 2001 From: ikkoham Date: Wed, 16 Nov 2022 23:13:19 +0900 Subject: [PATCH 7/8] fix docs --- .../gradients/finite_diff_estimator_gradient.py | 4 ++-- .../gradients/finite_diff_sampler_gradient.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index 0b63aff5b7b3..60ff8d9a7b24 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -49,7 +49,7 @@ def __init__( *, method: Literal["central", "forward", "backward"] = "central", ): - """ + r""" Args: estimator: The estimator used to compute the gradients. epsilon: The offset size for the finite difference gradients. @@ -60,7 +60,7 @@ def __init__( method: The computation method of the gradients. - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`, - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon. diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 54077a53c237..1b10883dec97 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -45,7 +45,7 @@ def __init__( *, method: Literal["central", "forward", "backward"] = "central", ): - """ + r""" Args: sampler: The sampler used to compute the gradients. epsilon: The offset size for the finite difference gradients. @@ -54,9 +54,11 @@ def __init__( default options > primitive's default setting. Higher priority setting overrides lower priority setting method: The computation method of the gradients. - - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, - - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` + + - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, + - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` + where :math:`e` is epsilon. Raises: From 0e5ca870a45a3196a40ded471e4d916af7c22322 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Wed, 16 Nov 2022 23:18:21 +0900 Subject: [PATCH 8/8] Update qiskit/algorithms/gradients/finite_diff_sampler_gradient.py Co-authored-by: Julien Gacon --- qiskit/algorithms/gradients/finite_diff_sampler_gradient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 1b10883dec97..784ffc427372 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -56,7 +56,7 @@ def __init__( method: The computation method of the gradients. - ``central`` computes :math:`\frac{f(x+e)-f(x-e)}{2e}`, - - ``forward`` computes math:`\frac{f(x+e) - f(x)}{e}`, + - ``forward`` computes :math:`\frac{f(x+e) - f(x)}{e}`, - ``backward`` computes :math:`\frac{f(x)-f(x-e)}{e}` where :math:`e` is epsilon.