Source code for qiskit_machine_learning.state_fidelities.compute_uncompute

# This code is part of a Qiskit project.
#
# (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
# 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.
"""
Compute-uncompute fidelity interface using primitives
"""

from __future__ import annotations
from collections.abc import Sequence
from copy import copy

from qiskit import QuantumCircuit
from qiskit.primitives import BaseSampler, BaseSamplerV1, SamplerResult
from qiskit.primitives.base import BaseSamplerV2
from qiskit.transpiler.passmanager import PassManager
from qiskit.result import QuasiDistribution
from qiskit.primitives.primitive_job import PrimitiveJob
from qiskit.providers import Options

from ..exceptions import AlgorithmError, QiskitMachineLearningError
from ..utils.deprecation import issue_deprecation_msg
from .base_state_fidelity import BaseStateFidelity
from .state_fidelity_result import StateFidelityResult
from ..algorithm_job import AlgorithmJob


[docs] class ComputeUncompute(BaseStateFidelity): r""" This class leverages the sampler primitive to calculate the state fidelity of two quantum circuits following the compute-uncompute method (see [1] for further reference). The fidelity can be defined as the state overlap. .. math:: |\langle\psi(x)|\phi(y)\rangle|^2 where :math:`x` and :math:`y` are optional parametrizations of the states :math:`\psi` and :math:`\phi` prepared by the circuits ``circuit_1`` and ``circuit_2``, respectively. **Reference:** [1] Havlíček, V., Córcoles, A. D., Temme, K., Harrow, A. W., Kandala, A., Chow, J. M., & Gambetta, J. M. (2019). Supervised learning with quantum-enhanced feature spaces. Nature, 567(7747), 209-212. `arXiv:1804.11326v2 [quant-ph] <https://arxiv.org/pdf/1804.11326.pdf>`_ """ def __init__( self, sampler: BaseSampler | BaseSamplerV2, *, options: Options | None = None, local: bool = False, pass_manager: PassManager | None = None, ) -> None: r""" Args: sampler: Sampler primitive instance. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > fidelity's default options > primitive's default setting. Higher priority setting overrides lower priority setting. local: If set to ``True``, the fidelity is averaged over single-qubit projectors .. math:: \hat{O} = \frac{1}{N}\sum_{i=1}^N|0_i\rangle\langle 0_i|, instead of the global projector :math:`|0\rangle\langle 0|^{\otimes n}`. This coincides with the standard (global) fidelity in the limit of the fidelity approaching 1. Might be used to increase the variance to improve trainability in algorithms such as :class:`~.time_evolvers.PVQD`. pass_manager: The pass manager to transpile the circuits, if necessary. Defaults to ``None``, as some primitives do not need transpiled circuits. Raises: ValueError: If the sampler is not an instance of ``BaseSampler``. """ if (not isinstance(sampler, BaseSampler)) and (not isinstance(sampler, BaseSamplerV2)): raise ValueError( f"The sampler should be an instance of BaseSampler or BaseSamplerV2, " f"but got {type(sampler)}" ) if isinstance(sampler, BaseSamplerV1): issue_deprecation_msg( msg="V1 Primitives are deprecated", version="0.8.0", remedy="Use V2 primitives for continued compatibility and support.", period="4 months", ) self._sampler: BaseSampler = sampler self._pass_manager = pass_manager self._local = local self._default_options = Options() if options is not None: self._default_options.update_options(**options) super().__init__()
[docs] def create_fidelity_circuit( self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit ) -> QuantumCircuit: """ Combines ``circuit_1`` and ``circuit_2`` to create the fidelity circuit following the compute-uncompute method. Args: circuit_1: (Parametrized) quantum circuit. circuit_2: (Parametrized) quantum circuit. Returns: The fidelity quantum circuit corresponding to circuit_1 and circuit_2. """ if len(circuit_1.clbits) > 0: circuit_1.remove_final_measurements() if len(circuit_2.clbits) > 0: circuit_2.remove_final_measurements() circuit = circuit_1.compose(circuit_2.inverse()) circuit.measure_all() if self._pass_manager is not None: circuit = self._pass_manager.run(circuit) return circuit
def _run( self, circuits_1: QuantumCircuit | Sequence[QuantumCircuit], circuits_2: QuantumCircuit | Sequence[QuantumCircuit], values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, **options, ) -> AlgorithmJob: r""" Computes the state overlap (fidelity) calculation between two (parametrized) circuits (first and second) for a specific set of parameter values (first and second) following the compute-uncompute method. Args: circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. values_1: Numerical parameters to be bound to the first circuits. values_2: Numerical parameters to be bound to the second circuits. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > fidelity's default options > primitive's default setting. Higher priority setting overrides lower priority setting. Returns: An AlgorithmJob for the fidelity calculation. Raises: ValueError: At least one pair of circuits must be defined. AlgorithmError: If the sampler job is not completed successfully. QiskitMachineLearningError: If the sampler is not an instance of ``BaseSamplerV1`` or ``BaseSamplerV2``. """ circuits = self._construct_circuits(circuits_1, circuits_2) if len(circuits) == 0: raise ValueError( "At least one pair of circuits must be defined to calculate the state overlap." ) values = self._construct_value_list(circuits_1, circuits_2, values_1, values_2) # The priority of run options is as follows: # options in `evaluate` method > fidelity's default options > # primitive's default options. opts = copy(self._default_options) opts.update_options(**options) if isinstance(self._sampler, BaseSamplerV1): sampler_job = self._sampler.run( circuits=circuits, parameter_values=values, **opts.__dict__ ) _len_quasi_dist = circuits[0].num_qubits local_opts = self._get_local_options(opts.__dict__) elif isinstance(self._sampler, BaseSamplerV2): sampler_job = self._sampler.run( [(circuits[i], values[i]) for i in range(len(circuits))], **opts.__dict__ ) if hasattr(circuits[0].layout, "_input_qubit_count"): _len_quasi_dist = circuits[0].layout._input_qubit_count else: _len_quasi_dist = circuits[0].num_qubits local_opts = opts.__dict__ else: raise QiskitMachineLearningError( "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got" + f" {type(self._sampler)} instead." ) return AlgorithmJob( ComputeUncompute._call, sampler_job, circuits, self._local, local_opts, self._sampler, self._post_process_v2, _len_quasi_dist, ) @staticmethod def _call( job: PrimitiveJob, circuits: Sequence[QuantumCircuit], local: bool, local_opts: Options = None, _sampler=None, _post_process_v2=None, num_virtual_qubits=None, ) -> StateFidelityResult: try: result = job.result() except Exception as exc: raise AlgorithmError("Sampler job failed!") from exc if isinstance(_sampler, BaseSamplerV1): quasi_dists = result.quasi_dists elif isinstance(_sampler, BaseSamplerV2): quasi_dists = _post_process_v2(result, num_virtual_qubits) if local: raw_fidelities = [ ComputeUncompute._get_local_fidelity( prob_dist, ( num_virtual_qubits if isinstance(_sampler, BaseSamplerV2) else circuit.num_qubits ), ) for prob_dist, circuit in zip(quasi_dists, circuits) ] else: raw_fidelities = [ ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in quasi_dists ] fidelities = ComputeUncompute._truncate_fidelities(raw_fidelities) return StateFidelityResult( fidelities=fidelities, raw_fidelities=raw_fidelities, metadata=result.metadata, options=local_opts, ) @property def options(self) -> Options: """Return the union of estimator options setting and fidelity default options, where, if the same field is set in both, the fidelity's default options override the primitive's default setting. Returns: The fidelity default + estimator options. """ return self._get_local_options(self._default_options.__dict__)
[docs] def update_default_options(self, **options): """Update the fidelity's default options setting. Args: **options: The fields to update the default options. """ self._default_options.update_options(**options)
def _get_local_options(self, options: Options) -> Options: """Return the union of the primitive's default setting, the fidelity default options, and the options in the ``run`` method. The order of priority is: options in ``run`` method > fidelity's default options > primitive's default setting. Args: options: The fields to update the options Returns: The fidelity default + estimator + run options. """ opts = copy(self._sampler.options) opts.update_options(**options) return opts def _post_process_v2(self, result: SamplerResult, num_virtual_qubits: int): quasis = [] for i in range(len(result)): bitstring_counts = result[i].data.meas.get_counts() # Normalize the counts to probabilities total_shots = sum(bitstring_counts.values()) probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} # Convert to quasi-probabilities counts = QuasiDistribution(probabilities) quasi_probs = {k: v for k, v in counts.items() if int(k) < 2**num_virtual_qubits} quasis.append(quasi_probs) return quasis @staticmethod def _get_global_fidelity(probability_distribution: dict[int, float]) -> float: """Process the probability distribution of a measurement to determine the global fidelity. Args: probability_distribution: Obtained from the measurement result Returns: The global fidelity. """ return probability_distribution.get(0, 0) @staticmethod def _get_local_fidelity(probability_distribution: dict[int, float], num_qubits: int) -> float: """Process the probability distribution of a measurement to determine the local fidelity by averaging over single-qubit projectors. Args: probability_distribution: Obtained from the measurement result Returns: The local fidelity. """ fidelity = 0.0 for qubit in range(num_qubits): for bitstring, prob in probability_distribution.items(): # Check whether the bit representing the current qubit is 0 if not bitstring >> qubit & 1: fidelity += prob / num_qubits return fidelity