# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2022, 2025.
#
# 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.abc import Sequence
import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.primitives import BaseEstimatorV2
from qiskit.providers import Options
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.transpiler.passmanager import BasePassManager
from ...exceptions import AlgorithmError
from ...utils import circuit_cache_key
from ..base.base_estimator_gradient import BaseEstimatorGradient
from ..base.estimator_gradient_result import EstimatorGradientResult
from ..utils import (
DerivativeType,
_make_lin_comb_gradient_circuit,
_make_lin_comb_observables,
)
[docs]
class LinCombEstimatorGradient(BaseEstimatorGradient):
"""Compute the gradients of the expectation values.
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>`_
"""
SUPPORTED_GATES = [
"rx",
"ry",
"rz",
"rzx",
"rzz",
"ryy",
"rxx",
"cx",
"cy",
"cz",
"ccx",
"swap",
"iswap",
"h",
"t",
"s",
"sdg",
"x",
"y",
"z",
]
def __init__(
self,
estimator: BaseEstimatorV2,
derivative_type: DerivativeType = DerivativeType.REAL,
options: Options | None = None,
pass_manager: BasePassManager | None = None,
):
r"""
Args:
estimator: The estimator used to compute the gradients.
derivative_type: The type of derivative. Can be either ``DerivativeType.REAL``
``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to
``DerivativeType.REAL``.
- ``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ω ψ(ω)〉`.
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.
pass_manager: The pass manager to transpile the circuits if necessary.
Defaults to ``None``, as some primitives do not need transpiled circuits.
"""
self._lin_comb_cache: dict[str | tuple, dict[Parameter, QuantumCircuit]] = {}
super().__init__(
estimator, options=options, derivative_type=derivative_type, pass_manager=pass_manager
)
@BaseEstimatorGradient.derivative_type.setter # type: ignore[attr-defined]
def derivative_type(self, derivative_type: DerivativeType) -> None:
"""Set the derivative type."""
self._derivative_type = derivative_type
def _run(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator],
parameter_values: Sequence[Sequence[float]] | np.ndarray,
parameters: Sequence[Sequence[Parameter]],
**options,
) -> EstimatorGradientResult:
"""Compute the estimator gradients on the given circuits."""
g_circuits, g_parameter_values, g_parameters = self._preprocess(
circuits, parameter_values, parameters, self.SUPPORTED_GATES
)
results = self._run_unique(
g_circuits, observables, g_parameter_values, g_parameters, **options
)
return self._postprocess(results, circuits, parameter_values, parameters)
def _run_unique(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator],
parameter_values: Sequence[Sequence[float]],
parameters: Sequence[Sequence[Parameter]],
**options,
) -> EstimatorGradientResult: # pragma: no cover
"""Compute the estimator gradients on the given circuits."""
job_circuits, job_observables, job_param_values, metadata = [], [], [], []
all_n = []
for circuit, observable, parameter_values_, parameters_ in zip(
circuits, observables, parameter_values, parameters
):
# Prepare circuits for the gradient of the specified parameters.
meta = {"parameters": parameters_}
circuit_key = circuit_cache_key(circuit)
if circuit_key not in self._lin_comb_cache:
# Cache the circuits for the linear combination of unitaries.
# We only cache the circuits for the specified parameters in the future.
self._lin_comb_cache[circuit_key] = _make_lin_comb_gradient_circuit(
circuit, add_measurement=False
)
lin_comb_circuits = self._lin_comb_cache[circuit_key]
gradient_circuits = []
for param_ in parameters_:
# TODO: the uuid attribute of param_ doesn't match that of param_match
# TODO: causing the two objects to not be identical, even if all other attributes match
param = param_
for param_match in lin_comb_circuits.keys():
if param_match.name == param_.name:
param = param_match
gradient_circuits.append(lin_comb_circuits[param])
n = len(gradient_circuits)
# Make the observable as :class:`~qiskit.quantum_info.SparsePauliOp` and
# add an ancillary operator to compute the gradient.
observable = SparsePauliOp(observable)
observable_1, observable_2 = _make_lin_comb_observables(
observable, self._derivative_type
)
# If its derivative type is `DerivativeType.COMPLEX`, calculate the gradient
# of the real and imaginary parts separately.
meta["derivative_type"] = self.derivative_type
metadata.append(meta)
# Combine inputs into a single job to reduce overhead.
if self._derivative_type == DerivativeType.COMPLEX:
job_circuits.extend(gradient_circuits * 2)
job_observables.extend([observable_1] * n + [observable_2] * n)
job_param_values.extend([parameter_values_] * 2 * n)
all_n.append(2 * n)
else:
job_circuits.extend(gradient_circuits)
job_observables.extend([observable_1] * n)
job_param_values.extend([parameter_values_] * n)
all_n.append(n)
if self._pass_manager is None:
circs = job_circuits
observables = job_observables
else:
circs = self._pass_manager.run(job_circuits)
observables = [op.apply_layout(circs[i].layout) for i, op in enumerate(job_observables)]
# Prepare circuit-observable-parameter tuples (PUBs)
circuit_observable_params = []
for pub in zip(circs, observables, job_param_values):
circuit_observable_params.append(pub)
# Run the estimator using PUBs and specified precision
job = self._estimator.run(circuit_observable_params)
try:
results = job.result()
except Exception as exc:
raise AlgorithmError("Estimator job failed.") from exc
results = np.array([float(r.data.evs) for r in results])
opt = Options(**options)
# Compute the gradients.
gradients = []
partial_sum_n = 0
for n in all_n:
# 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[partial_sum_n : partial_sum_n + n // 2]
gradient.imag = results[partial_sum_n + n // 2 : partial_sum_n + n]
else:
gradient = np.real(
results[partial_sum_n : partial_sum_n + n]
) # type: ignore[assignment, unused-ignore]
partial_sum_n += n
gradients.append(gradient)
return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt)