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

Efficient classical calculation of expectation gradients #9287

Merged
merged 19 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions qiskit/algorithms/gradients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
LinCombEstimatorGradient
ParamShiftEstimatorGradient
SPSAEstimatorGradient
ReverseEstimatorGradient
Sampler Gradients
=================
Expand All @@ -51,6 +52,7 @@
BaseQGT
LinCombQGT
QFI
ReverseQGT
Results
=======
Expand Down Expand Up @@ -81,6 +83,8 @@
from .sampler_gradient_result import SamplerGradientResult
from .spsa_estimator_gradient import SPSAEstimatorGradient
from .spsa_sampler_gradient import SPSASamplerGradient
from .reverse_gradient.reverse_gradient import ReverseEstimatorGradient
from .reverse_gradient.reverse_qgt import ReverseQGT

__all__ = [
"BaseEstimatorGradient",
Expand All @@ -101,4 +105,6 @@
"SamplerGradientResult",
"SPSAEstimatorGradient",
"SPSASamplerGradient",
"ReverseEstimatorGradient",
"ReverseQGT",
]
24 changes: 23 additions & 1 deletion qiskit/algorithms/gradients/base_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,43 @@ def __init__(
self,
estimator: BaseEstimator,
options: Options | None = None,
derivative_type: DerivativeType = DerivativeType.REAL,
):
"""
r"""
Args:
estimator: The estimator used to compute the 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
derivative_type: The type of derivative. Can be either ``DerivativeType.REAL``
``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``.
- ``DerivativeType.REAL`` computes :math:`2 \mathrm{Re}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`.
- ``DerivativeType.IMAG`` computes :math:`2 \mathrm{Im}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`.
- ``DerivativeType.COMPLEX`` computes :math:`2 ⟨ψ(ω)|O(θ)|dω ψ(ω)〉`.
Defaults to ``DerivativeType.REAL``, as this yields e.g. the commonly-used energy
gradient and this type is the only supported type for function-level schemes like
finite difference.
"""
self._estimator: BaseEstimator = estimator
self._default_options = Options()
if options is not None:
self._default_options.update_options(**options)
self._derivative_type = derivative_type

self._gradient_circuit_cache: dict[QuantumCircuit, GradientCircuit] = {}

@property
def derivative_type(self) -> DerivativeType:
"""Return the derivative type (real, imaginary or complex).
Returns:
The derivative type.
"""
return self._derivative_type

def run(
self,
circuits: Sequence[QuantumCircuit],
Expand Down
14 changes: 11 additions & 3 deletions qiskit/algorithms/gradients/lin_comb_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ def __init__(
Higher priority setting overrides lower priority setting.
"""
self._lin_comb_cache = {}
super().__init__(estimator, options, derivative_type=derivative_type)

@BaseEstimatorGradient.derivative_type.setter
def derivative_type(self, derivative_type: DerivativeType) -> None:
"""Set the derivative type."""
self._derivative_type = derivative_type
super().__init__(estimator, options)

def _run(
self,
Expand Down Expand Up @@ -143,7 +147,7 @@ def _run_unique(
)
# If its derivative type is `DerivativeType.COMPLEX`, calculate the gradient
# of the real and imaginary parts separately.
meta["derivative_type"] = self._derivative_type
meta["derivative_type"] = self.derivative_type
metadata.append(meta)
# Combine inputs into a single job to reduce overhead.
if self._derivative_type == DerivativeType.COMPLEX:
Expand Down Expand Up @@ -173,10 +177,14 @@ def _run_unique(
gradients = []
partial_sum_n = 0
for n in all_n:
if self._derivative_type == DerivativeType.COMPLEX:
# this disable is needed as Pylint does not understand derivative_type is a property if
# it is only defined in the base class and the getter is in the child
# pylint: disable=comparison-with-callable
if self.derivative_type == DerivativeType.COMPLEX:
gradient = np.zeros(n // 2, dtype="complex")
gradient.real = results.values[partial_sum_n : partial_sum_n + n // 2]
gradient.imag = results.values[partial_sum_n + n // 2 : partial_sum_n + n]

else:
gradient = np.real(results.values[partial_sum_n : partial_sum_n + n])
partial_sum_n += n
Expand Down
10 changes: 5 additions & 5 deletions qiskit/algorithms/gradients/lin_comb_qgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@


class LinCombQGT(BaseQGT):
"""Computes the Quantum Geometric Tensor (QGT) given a pure,
parameterized quantum state. This method employs a linear
combination of unitaries [1].
"""Computes the Quantum Geometric Tensor (QGT) given a pure, parameterized quantum state.
This method employs a linear combination of unitaries [1].
**Reference:**
[1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018
`arXiv:1811.11184 <https://arxiv.org/pdf/1811.11184.pdf>`_
[1]: Schuld et al., "Evaluating analytic gradients on quantum hardware" (2018).
`arXiv:1811.11184 <https://arxiv.org/pdf/1811.11184.pdf>`_
"""

SUPPORTED_GATES = [
Expand Down
11 changes: 11 additions & 0 deletions qiskit/algorithms/gradients/reverse_gradient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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.
53 changes: 53 additions & 0 deletions qiskit/algorithms/gradients/reverse_gradient/bind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022, 2023.
#
# 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.

"""Bind values to a parametrized circuit, accepting binds for non-existing parameters in the circuit."""

from __future__ import annotations
from collections.abc import Iterable

from qiskit.circuit import QuantumCircuit, Parameter

# pylint: disable=inconsistent-return-statements
def bind(
circuits: QuantumCircuit | Iterable[QuantumCircuit],
parameter_binds: dict[Parameter, float],
inplace: bool = False,
) -> QuantumCircuit | Iterable[QuantumCircuit] | None:
"""Bind parameters in a circuit (or list of circuits).
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
This method also allows passing parameter binds to parameters that are not in the circuit,
and thereby differs to :meth:`.QuantumCircuit.bind_parameters`.
Args:
circuits: Input circuit(s).
parameter_binds: A dictionary with ``{Parameter: float}`` pairs determining the values to
which the free parameters in the circuit(s) are bound.
inplace: If ``True``, bind the values in place, otherwise return circuit copies.
Returns:
The bound circuits, if ``inplace=False``, otherwise None.
"""
if not isinstance(circuits, Iterable):
circuits = [circuits]
return_list = False
else:
return_list = True

bound = []
for circuit in circuits:
existing_parameter_binds = {p: parameter_binds[p] for p in circuit.parameters}
bound.append(circuit.assign_parameters(existing_parameter_binds, inplace=inplace))

if not inplace:
return bound if return_list else bound[0]
156 changes: 156 additions & 0 deletions qiskit/algorithms/gradients/reverse_gradient/derive_circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022, 2023.
#
# 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.

"""Split a circuit into subcircuits, each containing a single parameterized gate."""

from __future__ import annotations
import itertools

from qiskit.circuit import QuantumCircuit, Parameter, Gate
from qiskit.circuit.library import RXGate, RYGate, RZGate, CRXGate, CRYGate, CRZGate


def gradient_lookup(gate: Gate) -> list[tuple[complex, QuantumCircuit]]:
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
"""Returns a circuit implementing the gradient of the input gate.
Args:
gate: The gate whose derivative is returned.
Returns:
The derivative of the input gate as list of ``(coeff, circuit)`` pairs,
where the sum of all ``coeff * circuit`` elements describes the full derivative.
The circuit is the unitary part of the derivative with a potential separate ``coeff``.
The output is a list as derivatives of e.g. controlled gates can only be described
as a sum of ``coeff * circuit`` pairs.
Raises:
NotImplementedError: If the derivative of ``gate`` is not implemented.
"""

param = gate.params[0]
if isinstance(gate, RXGate):
derivative = QuantumCircuit(gate.num_qubits)
derivative.rx(param, 0)
derivative.x(0)
return [(-0.5j, derivative)]
if isinstance(gate, RYGate):
derivative = QuantumCircuit(gate.num_qubits)
derivative.ry(param, 0)
derivative.y(0)
return [(-0.5j, derivative)]
if isinstance(gate, RZGate):
derivative = QuantumCircuit(gate.num_qubits)
derivative.rz(param, 0)
derivative.z(0)
return [(-0.5j, derivative)]
if isinstance(gate, CRXGate):
proj1 = QuantumCircuit(gate.num_qubits)
proj1.rx(param, 1)
proj1.x(1)

proj2 = QuantumCircuit(gate.num_qubits)
proj2.z(0)
proj2.rx(param, 1)
proj2.x(1)

return [(-0.25j, proj1), (0.25j, proj2)]
if isinstance(gate, CRYGate):
proj1 = QuantumCircuit(gate.num_qubits)
proj1.ry(param, 1)
proj1.y(1)

proj2 = QuantumCircuit(gate.num_qubits)
proj2.z(0)
proj2.ry(param, 1)
proj2.y(1)

return [(-0.25j, proj1), (0.25j, proj2)]
if isinstance(gate, CRZGate):
proj1 = QuantumCircuit(gate.num_qubits)
proj1.rz(param, 1)
proj1.z(1)

proj2 = QuantumCircuit(gate.num_qubits)
proj2.z(0)
proj2.rz(param, 1)
proj2.z(1)

return [(-0.25j, proj1), (0.25j, proj2)]
raise NotImplementedError("Cannot implement gradient for", gate)


def derive_circuit(
circuit: QuantumCircuit, parameter: Parameter
) -> list[tuple[complex, QuantumCircuit]]:
"""Return the analytic gradient expression of the input circuit wrt. a single parameter.
Returns a list of ``(coeff, gradient_circuit)`` tuples, where the derivative of the circuit is
given by the sum of the gradient circuits multiplied by their coefficient.
For example, the circuit::
┌───┐┌───────┐┌─────┐
q: ┤ H ├┤ Rx(x) ├┤ Sdg ├
└───┘└───────┘└─────┘
returns the coefficient `-0.5j` and the circuit equivalent to::
┌───┐┌───────┐┌───┐┌─────┐
q: ┤ H ├┤ Rx(x) ├┤ X ├┤ Sdg ├
└───┘└───────┘└───┘└─────┘
as the derivative of `Rx(x)` is `-0.5j Rx(x) X`.
Args:
circuit: The quantum circuit to derive.
parameter: The parameter with respect to which we derive.
Returns:
A list of ``(coeff, gradient_circuit)`` tuples.
Raises:
ValueError: If ``parameter`` is of the wrong type.
ValueError: If ``parameter`` is not in this circuit.
NotImplementedError: If a non-unique parameter is added, as the product rule is not yet
supported in this function.
"""
# this is added as useful user-warning, since sometimes ``ParameterExpression``s are
# passed around instead of ``Parameter``s
if not isinstance(parameter, Parameter):
raise ValueError(f"parameter must be of type Parameter, not {type(parameter)}.")

if parameter not in circuit.parameters:
raise ValueError(f"The parameter {parameter} is not in this circuit.")

if len(circuit._parameter_table[parameter]) > 1:
raise NotImplementedError("No product rule support yet, circuit parameters must be unique.")

summands, op_context = [], []
for i, op in enumerate(circuit.data):
gate = op[0]
op_context += [op[1:]]
if parameter in gate.params:
coeffs_and_grads = gradient_lookup(gate)
summands += [coeffs_and_grads]
else:
summands += [[(1, gate)]]

gradient = []
for product_rule_term in itertools.product(*summands):
summand_circuit = QuantumCircuit(*circuit.qregs)
c = 1
for i, term in enumerate(product_rule_term):
c *= term[0]
summand_circuit.data.append([term[1], *op_context[i]])
gradient += [(c, summand_circuit.copy())]

return gradient
Loading