Skip to content

Commit

Permalink
add spsa gradient
Browse files Browse the repository at this point in the history
  • Loading branch information
a-matsuo committed Aug 24, 2022
1 parent aeb76ff commit 876e50e
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 17 deletions.
6 changes: 6 additions & 0 deletions qiskit/algorithms/gradients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
LinCombSamplerGradient
ParamShiftEstimatorGradient
ParamShiftSamplerGradient
SPSAEstimatorGradient
SPSASamplerGradient
Results
=======
Expand All @@ -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",
Expand All @@ -71,4 +75,6 @@
"ParamShiftEstimatorGradient",
"ParamShiftSamplerGradient",
"SamplerGradientResult",
"SPSAEstimatorGradient",
"SPSASamplerGradient",
]
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion qiskit/algorithms/gradients/finite_diff_sampler_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion qiskit/algorithms/gradients/lin_comb_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion qiskit/algorithms/gradients/lin_comb_sampler_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 7 additions & 2 deletions qiskit/algorithms/gradients/param_shift_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 6 additions & 2 deletions qiskit/algorithms/gradients/param_shift_sampler_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
103 changes: 103 additions & 0 deletions qiskit/algorithms/gradients/spsa_estimator_gradient.py
Original file line number Diff line number Diff line change
@@ -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)
114 changes: 114 additions & 0 deletions qiskit/algorithms/gradients/spsa_sampler_gradient.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions qiskit/algorithms/gradients/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading

0 comments on commit 876e50e

Please sign in to comment.