diff --git a/qiskit/algorithms/__init__.py b/qiskit/algorithms/__init__.py index f7929e337c4d..966986f3b7ec 100644 --- a/qiskit/algorithms/__init__.py +++ b/qiskit/algorithms/__init__.py @@ -201,8 +201,18 @@ IterativePhaseEstimation +State Fidelities +---------------- + +Algorithms that compute the fidelity of pairs of quantum states. + +.. autosummary:: + :toctree: ../stubs/ + + state_fidelities + Exceptions -========== +---------- .. autosummary:: :toctree: ../stubs/ @@ -219,8 +229,19 @@ :toctree: ../stubs/ eval_observables -""" +Utility classes +--------------- + +Utility classes used by algorithms (mainly for type-hinting purposes). + +.. autosummary:: + :toctree: ../stubs/ + + AlgorithmJob + +""" +from .algorithm_job import AlgorithmJob from .algorithm_result import AlgorithmResult from .evolvers import EvolutionResult, EvolutionProblem from .evolvers.real_evolver import RealEvolver @@ -268,6 +289,7 @@ from .evolvers.pvqd import PVQD, PVQDResult __all__ = [ + "AlgorithmJob", "AlgorithmResult", "VariationalAlgorithm", "VariationalResult", diff --git a/qiskit/algorithms/algorithm_job.py b/qiskit/algorithms/algorithm_job.py new file mode 100644 index 000000000000..16db4df93dfc --- /dev/null +++ b/qiskit/algorithms/algorithm_job.py @@ -0,0 +1,24 @@ +# 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. + +""" +AlgorithmJob class +""" +from qiskit.primitives.primitive_job import PrimitiveJob + + +class AlgorithmJob(PrimitiveJob): + """ + This empty class is introduced for typing purposes. + """ + + pass diff --git a/qiskit/algorithms/state_fidelities/__init__.py b/qiskit/algorithms/state_fidelities/__init__.py new file mode 100644 index 000000000000..ea8e4e03bf89 --- /dev/null +++ b/qiskit/algorithms/state_fidelities/__init__.py @@ -0,0 +1,42 @@ +# 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. +""" +===================================================================== +State Fidelity Interfaces (:mod:`qiskit.algorithms.state_fidelities`) +===================================================================== + +.. currentmodule:: qiskit.algorithms.state_fidelities + +State Fidelities +================ + +.. autosummary:: + :toctree: ../stubs/ + + BaseStateFidelity + ComputeUncompute + +Results +======= + + .. autosummary:: + :toctree: ../stubs/ + + StateFidelityResult + +""" + +from .base_state_fidelity import BaseStateFidelity +from .compute_uncompute import ComputeUncompute +from .state_fidelity_result import StateFidelityResult + +__all__ = ["BaseStateFidelity", "ComputeUncompute", "StateFidelityResult"] diff --git a/qiskit/algorithms/state_fidelities/base_state_fidelity.py b/qiskit/algorithms/state_fidelities/base_state_fidelity.py new file mode 100644 index 000000000000..75f4d632396d --- /dev/null +++ b/qiskit/algorithms/state_fidelities/base_state_fidelity.py @@ -0,0 +1,306 @@ +# 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. +""" +Base state fidelity interface +""" + +from __future__ import annotations +from abc import ABC, abstractmethod +from collections.abc import Sequence, Mapping +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.algorithms import AlgorithmJob +from qiskit.circuit import ParameterVector +from .state_fidelity_result import StateFidelityResult + + +class BaseStateFidelity(ABC): + r""" + An interface to calculate state fidelities (state overlaps) for pairs of + (parametrized) quantum circuits. The calculation depends on the particular + fidelity method implementation, but can be always 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. + + """ + + def __init__(self) -> None: + + # use cache for preventing unnecessary circuit compositions + self._circuit_cache: Mapping[(int, int), QuantumCircuit] = {} + + @staticmethod + def _preprocess_values( + circuits: QuantumCircuit | Sequence[QuantumCircuit], + values: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> Sequence[Sequence[float]]: + """ + Checks whether the passed values match the shape of the parameters + of the corresponding circuits and formats values to 2D list. + + Args: + circuits: List of circuits to be checked. + values: Parameter values corresponding to the circuits to be checked. + + Returns: + A 2D value list if the values match the circuits, or an empty 2D list + if values is None. + + Raises: + ValueError: if the number of parameter values doesn't match the number of + circuit parameters + TypeError: if the input values are not a sequence. + """ + + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + + if values is None: + for circuit in circuits: + if circuit.num_parameters != 0: + raise ValueError( + f"`values` cannot be `None` because circuit <{circuit.name}> has " + f"{circuit.num_parameters} free parameters." + ) + return [[]] + else: + + # Support ndarray + if isinstance(values, np.ndarray): + values = values.tolist() + if len(values) > 0 and isinstance(values[0], np.ndarray): + values = [v.tolist() for v in values] + + if not isinstance(values, Sequence): + raise TypeError( + f"Expected a sequence of numerical parameter values, " + f"but got input type {type(values)} instead." + ) + + # ensure 2d + if len(values) > 0 and not isinstance(values[0], Sequence): + values = [values] + return values + + def _check_qubits_match(self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit) -> None: + """ + Checks that the number of qubits of 2 circuits matches. + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Raises: + ValueError: when ``circuit_1`` and ``circuit_2`` don't have the + same number of qubits. + """ + + if circuit_1.num_qubits != circuit_2.num_qubits: + raise ValueError( + f"The number of qubits for the first circuit ({circuit_1.num_qubits}) " + f"and second circuit ({circuit_2.num_qubits}) are not the same." + ) + + @abstractmethod + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + """ + Implementation-dependent method to create a fidelity circuit + from 2 circuit inputs. + + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Returns: + The fidelity quantum circuit corresponding to ``circuit_1`` and ``circuit_2``. + """ + raise NotImplementedError + + def _construct_circuits( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + ) -> Sequence[QuantumCircuit]: + """ + Constructs the list of fidelity circuits to be evaluated. + These circuits represent the state overlap between pairs of input circuits, + and their construction depends on the fidelity method implementations. + + Args: + circuits_1: (Parametrized) quantum circuits. + circuits_2: (Parametrized) quantum circuits. + + Returns: + List of constructed fidelity circuits. + + Raises: + ValueError: if the length of the input circuit lists doesn't match. + """ + + if isinstance(circuits_1, QuantumCircuit): + circuits_1 = [circuits_1] + if isinstance(circuits_2, QuantumCircuit): + circuits_2 = [circuits_2] + + if len(circuits_1) != len(circuits_2): + raise ValueError( + f"The length of the first circuit list({len(circuits_1)}) " + f"and second circuit list ({len(circuits_2)}) is not the same." + ) + + circuits = [] + for (circuit_1, circuit_2) in zip(circuits_1, circuits_2): + + # TODO: improve caching, what if the circuit is modified without changing the id? + circuit = self._circuit_cache.get((id(circuit_1), id(circuit_2))) + + if circuit is not None: + circuits.append(circuit) + else: + self._check_qubits_match(circuit_1, circuit_2) + + # re-parametrize input circuits + # TODO: make smarter checks to avoid unnecesary reparametrizations + parameters_1 = ParameterVector("x", circuit_1.num_parameters) + parametrized_circuit_1 = circuit_1.assign_parameters(parameters_1) + parameters_2 = ParameterVector("y", circuit_2.num_parameters) + parametrized_circuit_2 = circuit_2.assign_parameters(parameters_2) + + circuit = self.create_fidelity_circuit( + parametrized_circuit_1, parametrized_circuit_2 + ) + circuits.append(circuit) + # update cache + self._circuit_cache[id(circuit_1), id(circuit_2)] = circuit + + return circuits + + def _construct_value_list( + self, + circuits_1: Sequence[QuantumCircuit], + circuits_2: Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> list[float]: + """ + Preprocesses input parameter values to match the fidelity + circuit parametrization, and return in list format. + + Args: + circuits_1: (Parametrized) quantum circuits preparing the + first list of quantum states. + circuits_2: (Parametrized) quantum circuits preparing the + second list of quantum states. + values_1: Numerical parameters to be bound to the first circuits. + values_2: Numerical parameters to be bound to the second circuits. + + Returns: + List of parameter values for fidelity circuit. + + """ + values_1 = self._preprocess_values(circuits_1, values_1) + values_2 = self._preprocess_values(circuits_2, values_2) + + values = [] + if len(values_2[0]) == 0: + values = list(values_1) + elif len(values_1[0]) == 0: + values = list(values_2) + else: + for (val_1, val_2) in zip(values_1, values_2): + values.append(val_1 + val_2) + + return values + + @abstractmethod + 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, + **run_options, + ) -> StateFidelityResult: + r""" + Computes the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). + + 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 set of circuits + values_2: Numerical parameters to be bound to the second set of circuits. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The result of the fidelity calculation. + """ + raise NotImplementedError + + 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, + **run_options, + ) -> AlgorithmJob: + r""" + Runs asynchronously the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). This calculation depends on the particular + fidelity method implementation. + + 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 set of circuits. + values_2: Numerical parameters to be bound to the second set of circuits. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + Primitive job for the fidelity calculation. + The job's result is an instance of ``StateFidelityResult``. + """ + + job = AlgorithmJob(self._run, circuits_1, circuits_2, values_1, values_2, **run_options) + + job.submit() + return job + + def _truncate_fidelities(self, fidelities: Sequence[float]) -> Sequence[float]: + """ + Ensures fidelity result in [0,1]. + + Args: + fidelities: Sequence of raw fidelity results. + + Returns: + List of truncated fidelities. + + """ + return np.clip(fidelities, 0, 1).tolist() diff --git a/qiskit/algorithms/state_fidelities/compute_uncompute.py b/qiskit/algorithms/state_fidelities/compute_uncompute.py new file mode 100644 index 000000000000..ff9080e5d3ba --- /dev/null +++ b/qiskit/algorithms/state_fidelities/compute_uncompute.py @@ -0,0 +1,145 @@ +# 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. +""" +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.algorithms import AlgorithmError +from qiskit.primitives import BaseSampler + +from .base_state_fidelity import BaseStateFidelity +from .state_fidelity_result import StateFidelityResult + + +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] `_ + + """ + + def __init__(self, sampler: BaseSampler, **run_options) -> None: + """ + Args: + sampler: Sampler primitive instance. + run_options: Backend runtime options used for circuit execution. + + Raises: + ValueError: If the sampler is not an instance of ``BaseSampler``. + """ + if not isinstance(sampler, BaseSampler): + raise ValueError( + f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" + ) + self._sampler: BaseSampler = sampler + self._default_run_options = run_options + super().__init__() + + 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. + """ + circuit = circuit_1.compose(circuit_2.inverse()) + circuit.measure_all() + 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, + **run_options, + ) -> StateFidelityResult: + 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. + run_options: Backend runtime options used for circuit execution. The order + of priority is\: run_options in ``run`` method > fidelity's default + run_options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The result of the fidelity calculation. + + Raises: + ValueError: At least one pair of circuits must be defined. + AlgorithmError: If the sampler job is not completed successfully. + """ + + 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: + # run_options in `evaluate` method > fidelity's default run_options > + # primitive's default run_options. + run_opts = copy(self._default_run_options) + run_opts.update(**run_options) + + job = self._sampler.run(circuits=circuits, parameter_values=values, **run_opts) + + try: + result = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed!") from exc + + raw_fidelities = [prob_dist.get(0, 0) for prob_dist in result.quasi_dists] + fidelities = self._truncate_fidelities(raw_fidelities) + + return StateFidelityResult( + fidelities=fidelities, + raw_fidelities=raw_fidelities, + metadata=result.metadata, + run_options=run_opts, + ) diff --git a/qiskit/algorithms/state_fidelities/state_fidelity_result.py b/qiskit/algorithms/state_fidelities/state_fidelity_result.py new file mode 100644 index 000000000000..04d4aa0ca411 --- /dev/null +++ b/qiskit/algorithms/state_fidelities/state_fidelity_result.py @@ -0,0 +1,35 @@ +# 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. +""" +Fidelity result class +""" + +from __future__ import annotations + +from collections.abc import Sequence, Mapping +from typing import Any +from dataclasses import dataclass + + +@dataclass(frozen=True) +class StateFidelityResult: + """This class stores the result of StateFidelity computations.""" + + fidelities: Sequence[float] + """List of truncated fidelity values for each pair of input circuits, ensured to be in [0,1].""" + raw_fidelities: Sequence[float] + """List of raw fidelity values for each pair of input circuits, which might not be in [0,1] + depending on the error mitigation method used.""" + metadata: Sequence[Mapping[str, Any]] + """Additional information about the fidelity calculation.""" + run_options: Mapping[str, Any] + """Runtime options for the execution of the fidelity job.""" diff --git a/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml b/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml new file mode 100644 index 000000000000..96e1977096ba --- /dev/null +++ b/releasenotes/notes/add-fidelity-interface-primitives-dc543d079ecaa8dd.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Add new algorithms to calculate state fidelities/overlaps + for pairs of quantum circuits (that can be parametrized). Apart from + the base class (:class:`qiskit.algorithms.state_fidelities.BaseStateFidelity`), + there is now an implementation of the compute-uncompute method that leverages + the sampler primitive (:class:`qiskit.algorithms.state_fidelities.ComputeUncompute`). + + Example:: + .. code-block:: python + + import numpy as np + from qiskit.primitives import Sampler + from qiskit.algorithms.state_fidelities import ComputeUncompute + from qiskit. import RealAmplitudes + + sampler = Sampler(...) + fidelity = ComputeUncompute(sampler) + circuit = RealAmplitudes(2) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + job = fidelity.run([circuit], [circuit], [values], [values+shift]) + fidelities = job.result().fidelities + + diff --git a/test/python/algorithms/state_fidelities/__init__.py b/test/python/algorithms/state_fidelities/__init__.py new file mode 100644 index 000000000000..d8b7d587c4cc --- /dev/null +++ b/test/python/algorithms/state_fidelities/__init__.py @@ -0,0 +1,13 @@ +# 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. + +"""Tests for the primitive-based fidelity interfaces.""" diff --git a/test/python/algorithms/state_fidelities/test_compute_uncompute.py b/test/python/algorithms/state_fidelities/test_compute_uncompute.py new file mode 100644 index 000000000000..d4a605dbb964 --- /dev/null +++ b/test/python/algorithms/state_fidelities/test_compute_uncompute.py @@ -0,0 +1,217 @@ +# 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. + +"""Tests for Fidelity.""" + +import unittest + +import numpy as np + +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Sampler +from qiskit.algorithms.state_fidelities import ComputeUncompute +from qiskit.test import QiskitTestCase +from qiskit import QiskitError + + +class TestComputeUncompute(QiskitTestCase): + """Test Compute-Uncompute Fidelity class""" + + def setUp(self): + super().setUp() + parameters = ParameterVector("x", 2) + + rx_rotations = QuantumCircuit(2) + rx_rotations.rx(parameters[0], 0) + rx_rotations.rx(parameters[1], 1) + + ry_rotations = QuantumCircuit(2) + ry_rotations.ry(parameters[0], 0) + ry_rotations.ry(parameters[1], 1) + + plus = QuantumCircuit(2) + plus.h([0, 1]) + + zero = QuantumCircuit(2) + + rx_rotation = QuantumCircuit(2) + rx_rotation.rx(parameters[0], 0) + rx_rotation.h(1) + + self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] + self._sampler = Sampler() + self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) + self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) + + def test_1param_pair(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_4param_pairs(self): + """test for fidelity with four pairs of parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-16) + + def test_symmetry(self): + """test for fidelity with the same circuit""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job_1 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params + ) + job_2 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params + ) + results_1 = job_1.result() + results_2 = job_2.result() + np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-16) + + def test_no_params(self): + """test for fidelity without parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run([self._circuit[2]], [self._circuit[3]]) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + + def test_left_param(self): + """test for fidelity with only left parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_right_param(self): + """test for fidelity with only right parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_not_set_circuits(self): + """test for fidelity with no circuits.""" + fidelity = ComputeUncompute(self._sampler) + with self.assertRaises(TypeError): + job = fidelity.run( + circuits_1=None, + circuits_2=None, + values_1=self._left_params, + values_2=self._right_params, + ) + job.result() + + def test_circuit_mismatch(self): + """test for fidelity with different number of left/right circuits.""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + with self.assertRaises(ValueError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * (n + 1), + self._left_params, + self._right_params, + ) + job.result() + + def test_param_mismatch(self): + """test for fidelity with different number of left/right parameters that + do not match the circuits'.""" + + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + with self.assertRaises(QiskitError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * n, + self._left_params, + self._right_params[:-2], + ) + job.result() + + with self.assertRaises(QiskitError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * n, + self._left_params[:-2], + self._right_params[:-2], + ) + job.result() + + with self.assertRaises(ValueError): + job = fidelity.run([self._circuit[0]] * n, [self._circuit[1]] * n) + job.result() + + def test_asymmetric_params(self): + """test for fidelity when the 2 circuits have different number of + left/right parameters.""" + + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + right_params = [[p] for p in self._right_params[:, 0]] + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[4]] * n, + self._left_params, + right_params, + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-16) + + def test_input_format(self): + """test for different input format variations""" + + fidelity = ComputeUncompute(self._sampler) + circuit = RealAmplitudes(2) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + # lists of circuits, lists of numpy arrays + job = fidelity.run([circuit], [circuit], [values], [values + shift]) + result_1 = job.result() + + # lists of circuits, lists of lists + shift_val = values + shift + job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) + result_2 = job.result() + + # circuits, lists + shift_val = values + shift + job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) + result_3 = job.result() + + # circuits, np.arrays + job = fidelity.run(circuit, circuit, values, values + shift) + result_4 = job.result() + + np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-16) + + +if __name__ == "__main__": + unittest.main()