Source code for qiskit_algorithms.gradients.lin_comb.lin_comb_qgt

# This code is part of a Qiskit project.
#
# (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.
"""
A class for the Linear Combination Quantum Gradient Tensor.
"""

from __future__ import annotations

from collections.abc import Sequence

import numpy as np

from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.primitives import BaseEstimator
from qiskit.primitives.utils import _circuit_key
from qiskit.providers import Options
from qiskit.quantum_info import SparsePauliOp

from ..base.base_qgt import BaseQGT
from .lin_comb_estimator_gradient import LinCombEstimatorGradient
from ..base.qgt_result import QGTResult
from ..utils import DerivativeType, _make_lin_comb_qgt_circuit, _make_lin_comb_observables

from ...exceptions import AlgorithmError


[docs]class LinCombQGT(BaseQGT): """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>`_ """ 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: BaseEstimator, phase_fix: bool = True, derivative_type: DerivativeType = DerivativeType.COMPLEX, options: Options | None = None, ): r""" Args: estimator: The estimator used to compute the QGT. phase_fix: Whether to calculate the second term (phase fix) of the QGT, which is :math:`\langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle`. Default to ``True``. derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to ``DerivativeType.REAL``. - ``DerivativeType.REAL`` computes .. math:: \mathrm{Re(QGT)}_{ij}= \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - ``DerivativeType.IMAG`` computes .. math:: \mathrm{Re(QGT)}_{ij}= \mathrm{Im}[\langle \partial_i \psi | \partial_j \psi \rangle - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. - ``DerivativeType.COMPLEX`` computes .. math:: \mathrm{QGT}_{ij}= [\langle \partial_i \psi | \partial_j \psi \rangle - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. options: Backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > QGT's default options > primitive's default setting. Higher priority setting overrides lower priority setting. """ super().__init__(estimator, phase_fix, derivative_type, options=options) self._gradient = LinCombEstimatorGradient( estimator, derivative_type=DerivativeType.COMPLEX, options=options ) self._lin_comb_qgt_circuit_cache: dict[ tuple, dict[tuple[Parameter, Parameter], QuantumCircuit] ] = {} def _run( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter]], **options, ) -> QGTResult: """Compute the QGT 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, g_parameter_values, g_parameters, **options) return self._postprocess(results, circuits, parameter_values, parameters) def _run_unique( self, circuits: Sequence[QuantumCircuit], parameter_values: Sequence[Sequence[float]], parameters: Sequence[Sequence[Parameter]], **options, ) -> QGTResult: """Compute the QGTs on the given circuits.""" job_circuits, job_observables, job_param_values, metadata = [], [], [], [] all_n, all_m = [], [] phase_fixes: list[int | np.ndarray] = [] for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): # Prepare circuits for the gradient of the specified parameters. parameters_ = [p for p in circuit.parameters if p in parameters_] meta = {"parameters": parameters_} metadata.append(meta) # Compute the first term in the QGT circuit_key = _circuit_key(circuit) if circuit_key not in self._lin_comb_qgt_circuit_cache: # generate the all of the circuits for the first term in the QGT and cache them. # Only the circuit related to specified parameters will be executed. # In the future, we can generate the specified circuits on demand. self._lin_comb_qgt_circuit_cache[circuit_key] = _make_lin_comb_qgt_circuit(circuit) lin_comb_qgt_circuits = self._lin_comb_qgt_circuit_cache[circuit_key] qgt_circuits = [] rows, cols = np.triu_indices(len(parameters_)) for row, col in zip(rows, cols): param_i = parameters_[row] param_j = parameters_[col] qgt_circuits.append(lin_comb_qgt_circuits[(param_i, param_j)]) observable = SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) observable_1, observable_2 = _make_lin_comb_observables( observable, self._derivative_type ) n = len(qgt_circuits) if self._derivative_type == DerivativeType.COMPLEX: job_circuits.extend(qgt_circuits * 2) job_observables.extend([observable_1] * n + [observable_2] * n) job_param_values.extend([parameter_values_] * 2 * n) all_m.append(len(parameters_)) all_n.append(2 * n) else: job_circuits.extend(qgt_circuits) job_observables.extend([observable_1] * n) job_param_values.extend([parameter_values_] * n) all_m.append(len(parameters_)) all_n.append(n) # Run the single job with all circuits. job = self._estimator.run( job_circuits, job_observables, job_param_values, **options, ) if self._phase_fix: # Compute the second term in the QGT if phase fix is enabled. phase_fix_obs = [ SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) for circuit in circuits ] phase_fix_job = self._gradient.run( circuits=circuits, observables=phase_fix_obs, parameter_values=parameter_values, parameters=parameters, **options, ) try: results = job.result() if self._phase_fix: gradient_results = phase_fix_job.result() except AlgorithmError as exc: raise AlgorithmError("Estimator job or gradient job failed.") from exc # Compute the phase fix if self._phase_fix: for gradient in gradient_results.gradients: phase_fix = np.outer(np.conjugate(gradient), gradient) # Select the real or imaginary part of the phase fix if needed if self.derivative_type == DerivativeType.REAL: phase_fix = np.real(phase_fix) elif self.derivative_type == DerivativeType.IMAG: phase_fix = np.imag(phase_fix) phase_fixes.append(phase_fix) else: phase_fixes = [0 for i in range(len(circuits))] # Compute the QGT qgts = [] partial_sum_n = 0 for i, (n, m) in enumerate(zip(all_n, all_m)): qgt = np.zeros((m, m), dtype="complex") # Compute the first term in the QGT if self.derivative_type == DerivativeType.COMPLEX: qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n // 2] qgt[np.triu_indices(m)] += ( 1j * results.values[partial_sum_n + n // 2 : partial_sum_n + n] ) elif self.derivative_type == DerivativeType.REAL: qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n] elif self.derivative_type == DerivativeType.IMAG: qgt[np.triu_indices(m)] = 1j * results.values[partial_sum_n : partial_sum_n + n] # Add the conjugate of the upper triangle to the lower triangle qgt += np.triu(qgt, k=1).conjugate().T if self.derivative_type == DerivativeType.REAL: qgt = np.real(qgt) elif self.derivative_type == DerivativeType.IMAG: qgt = np.imag(qgt) # Subtract the phase fix from the QGT qgt = qgt - phase_fixes[i] partial_sum_n += n qgts.append(qgt / 4) opt = self._get_local_options(options) return QGTResult( qgts=qgts, derivative_type=self.derivative_type, metadata=metadata, options=opt )