Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for EstimatorV2 primitives #48

Open
wants to merge 14 commits into
base: update-V2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2022, 2024.
# (C) Copyright IBM 2022, 2024
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info.operators.base_operator import BaseOperator

from qiskit.primitives import BaseEstimatorV1
from qiskit.primitives.base import BaseEstimatorV2
from numpy import array
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

from ..base.base_estimator_gradient import BaseEstimatorGradient
from ..base.estimator_gradient_result import EstimatorGradientResult
from ..utils import _make_param_shift_parameter_values
Expand Down Expand Up @@ -97,26 +101,45 @@ def _run_unique(
job_param_values.extend(param_shift_parameter_values)
all_n.append(n)

# Run the single job with all circuits.
job = self._estimator.run(
job_circuits,
job_observables,
job_param_values,
**options,
)
PUBs = [(job_circuits[i],[observable],job_param_values[i]) for i in range(n)]
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(self._estimator, BaseEstimatorV1):
# Run the single job with all circuits.
job = self._estimator.run(
job_circuits,
job_observables,
job_param_values,
**options,
)
results = job.result()
elif isinstance(self._estimator, BaseEstimatorV2):
job = self._estimator.run(PUBs, precision=0.001, **options)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

precision parameter might introduce issues for tests, we need to think about how to make stable tests for V2.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qiskit uses a default value of 0.0: https://github.com/Qiskit/qiskit/blob/8ccbc8d28f34e3f2e1e0e8cbb66c4da95754b341/qiskit/primitives/statevector_estimator.py#L113
So to make tests reproducible we could fetch

precision=self._default_precision

Since we are already within the BaseEstimatorV2 case, we can assume that _default_precision is defined in the superclass.

else:
raise AlgorithmError("Wrong Estimator Type.")
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

try:
results = job.result()
except Exception as exc:
raise AlgorithmError("Estimator job failed.") from exc


# Compute the gradients.
gradients = []
partial_sum_n = 0
for n in all_n:
result = results.values[partial_sum_n : partial_sum_n + n]
gradient_ = (result[: n // 2] - result[n // 2 :]) / 2
gradients.append(gradient_)
partial_sum_n += n

opt = self._get_local_options(options)
if isinstance(self._estimator, BaseEstimatorV1):
result = results.values[partial_sum_n : partial_sum_n + n]
gradient_ = (result[: n // 2] - result[n // 2 :]) / 2
gradients.append(gradient_)
partial_sum_n += n
opt = self._get_local_options(options)

if isinstance(self._estimator, BaseEstimatorV2):
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
result = array([float(result.data.evs[0]) for result in results])
gradient_ = (result[: n // 2] - result[n // 2 :]) / 2
gradients.append(gradient_)
partial_sum_n += n
opt = options
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt)


70 changes: 48 additions & 22 deletions qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2022, 2024.
# (C) Copyright IBM 2022, 2023.
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
#
# 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
Expand All @@ -23,6 +23,10 @@
from qiskit.providers import Options
from qiskit.quantum_info.operators.base_operator import BaseOperator

from qiskit.primitives import BaseEstimatorV1
from qiskit.primitives.base import BaseEstimatorV2
from numpy import array
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

from ..base.base_estimator_gradient import BaseEstimatorGradient
from ..base.estimator_gradient_result import EstimatorGradientResult

Expand Down Expand Up @@ -102,33 +106,55 @@ def _run(
job_param_values.extend(plus + minus)
all_n.append(2 * self._batch_size)

# Run the single job with all circuits.
job = self._estimator.run(
job_circuits,
job_observables,
job_param_values,
**options,
)
PUBs = [(job_circuits[i],[observable],job_param_values[i]) for i in range(len(job_circuits))]

if isinstance(self._estimator, BaseEstimatorV1):
# Run the single job with all circuits.
job = self._estimator.run(
job_circuits,
job_observables,
job_param_values,
**options,
)
results = job.result()
elif isinstance(self._estimator, BaseEstimatorV2):
job = self._estimator.run(PUBs, precision=0.001, **options)
else:
raise AlgorithmError("Wrong Estimator Type.")

try:
results = job.result()
except Exception as exc:
raise AlgorithmError("Estimator job failed.") from exc

# Compute the gradients.
gradients = []
partial_sum_n = 0
for i, n in enumerate(all_n):
result = results.values[partial_sum_n : partial_sum_n + n]
partial_sum_n += n
n = len(result) // 2
diffs = (result[:n] - result[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])

opt = self._get_local_options(options)
if isinstance(self._estimator, BaseEstimatorV1):
result = results.values[partial_sum_n : partial_sum_n + n]
partial_sum_n += n
n = len(result) // 2
diffs = (result[:n] - result[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])
opt = self._get_local_options(options)
if isinstance(self._estimator, BaseEstimatorV2):
result = array([float(result.data.evs[0]) for result in results])
partial_sum_n += n
n = len(result) // 2
diffs = (result[:n] - result[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])
opt = options
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt)
57 changes: 37 additions & 20 deletions qiskit_machine_learning/neural_networks/estimator_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@
from copy import copy
from typing import Sequence

from qiskit.primitives import BaseEstimatorV1
from qiskit.primitives.base import BaseEstimatorV2

import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator
from ..gradients import (
from qiskit_algorithms.gradients import (
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
BaseEstimatorGradient,
EstimatorGradientResult,
ParamShiftEstimatorGradient,
SPSAEstimatorGradient,
)

from ..circuit.library import QNNCircuit
from ..exceptions import QiskitMachineLearningError
from qiskit_machine_learning.circuit.library import QNNCircuit
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
from qiskit_machine_learning.exceptions import QiskitMachineLearningError
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

from .neural_network import NeuralNetwork

Expand Down Expand Up @@ -111,6 +115,7 @@ def __init__(
weight_params: Sequence[Parameter] | None = None,
gradient: BaseEstimatorGradient | None = None,
input_gradients: bool = False,
num_qubits: int | None = None,
):
r"""
Args:
Expand All @@ -136,11 +141,12 @@ def __init__(
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters.
gradient: The estimator gradient to be used for the backward pass.
If None, a default instance of the estimator gradient,
:class:`~qiskit_machine_learning.gradients.ParamShiftEstimatorGradient`, will be used.
:class:`~qiskit_algorithms.gradients.ParamShiftEstimatorGradient`, will be used.
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
input_gradients: Determines whether to compute gradients with respect to input data.
Note that this parameter is ``False`` by default, and must be explicitly set to
``True`` for a proper gradient computation when using
:class:`~qiskit_machine_learning.connectors.TorchConnector`.
num_qubits: Number of qubits for actual circuit.

Raises:
QiskitMachineLearningError: Invalid parameter values.
Expand All @@ -149,8 +155,13 @@ def __init__(
estimator = Estimator()
self.estimator = estimator
self._org_circuit = circuit
if num_qubits is None:
self.num_qubits = circuit.num_qubits
print('Warning: No number of qubits provided, using it from provided circuit, if the circuit is transpiled this can be wrong and cause issues.')
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required since we might lose information about actual number of qubits of the circuit after the transpilation, not sure if it can simply be retrieved from transpiled_circuit.

else:
self.num_qubits = num_qubits
if observables is None:
observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)])
observables = SparsePauliOp.from_list([("Z" * num_qubits, 1)])
if isinstance(observables, BaseOperator):
observables = (observables,)
self._observables = observables
Expand Down Expand Up @@ -208,23 +219,31 @@ def input_gradients(self, input_gradients: bool) -> None:

def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray:
"""Post-processing during forward pass of the network."""
return np.reshape(result.values, (-1, num_samples)).T
return np.reshape(result, (-1, num_samples)).T

def _forward(
self, input_data: np.ndarray | None, weights: np.ndarray | None
) -> np.ndarray | None:
"""Forward pass of the neural network."""
parameter_values_, num_samples = self._preprocess_forward(input_data, weights)
job = self.estimator.run(
[self._circuit] * num_samples * self.output_shape[0],
[op for op in self._observables for _ in range(num_samples)],
np.tile(parameter_values_, (self.output_shape[0], 1)),
)
try:
results = job.result()
except Exception as exc:
raise QiskitMachineLearningError("Estimator job failed.") from exc

if isinstance(self.estimator, BaseEstimatorV1):
job = self.estimator.run(
[self._circuit] * num_samples * self.output_shape[0],
[op for op in self._observables for _ in range(num_samples)],
np.tile(parameter_values_, (self.output_shape[0], 1)),
)
results = job.result().values
elif isinstance(self.estimator, BaseEstimatorV2):
PUBs = []
for _ in range(num_samples):
for observable in self._observables:
PUBs.append((self._circuit, [observable], parameter_values_))
job = self.estimator.run(PUBs, precision=0.001)
results = job.result()
results = [result.data.evs[0] for result in results]
else:
raise QiskitMachineLearningError("Wrong Estimator Type.")
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
return self._forward_postprocess(num_samples, results)

def _backward_postprocess(
Expand Down Expand Up @@ -270,12 +289,10 @@ def _backward(

job = None
if self._input_gradients:
job = self.gradient.run(circuits, observables, param_values) # type: ignore[arg-type]
job = self.gradient.run(circuits, observables, param_values)
elif len(parameter_values[0]) > self._num_inputs:
params = [self._circuit.parameters[self._num_inputs :]] * num_circuits
job = self.gradient.run(
circuits, observables, param_values, parameters=params # type: ignore[arg-type]
)
job = self.gradient.run(circuits, observables, param_values, parameters=params)
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved

if job is not None:
try:
Expand All @@ -285,4 +302,4 @@ def _backward(

input_grad, weights_grad = self._backward_postprocess(num_samples, results)

return input_grad, weights_grad
return input_grad, weights_grad
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved
OkuyanBoga marked this conversation as resolved.
Show resolved Hide resolved