diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index cdfdde87a56..83967600153 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -387,14 +387,15 @@ Primitives API ============== -Primitives V2 -------------- +Estimator V2 +------------ .. autosummary:: :toctree: ../stubs/ BaseEstimatorV2 StatevectorEstimator + BackendEstimatorV2 Sampler V2 ---------- @@ -473,3 +474,4 @@ from .sampler import Sampler from .statevector_estimator import StatevectorEstimator from .statevector_sampler import StatevectorSampler +from .backend_estimator_v2 import BackendEstimatorV2 diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 50016dfe7a6..23556e2efe2 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -413,14 +413,12 @@ def _paulis2inds(paulis: PauliList) -> list[int]: # Treat Z, X, Y the same nonid = paulis.z | paulis.x - inds = [0] * paulis.size # bits are packed into uint8 in little endian # e.g., i-th bit corresponds to coefficient 2^i packed_vals = np.packbits(nonid, axis=1, bitorder="little") - for i, vals in enumerate(packed_vals): - for j, val in enumerate(vals): - inds[i] += val.item() * (1 << (8 * j)) - return inds + power_uint8 = 1 << (8 * np.arange(packed_vals.shape[1], dtype=object)) + inds = packed_vals @ power_uint8 + return inds.tolist() def _parity(integer: int) -> int: diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py new file mode 100644 index 00000000000..91e45a16e98 --- /dev/null +++ b/qiskit/primitives/backend_estimator_v2.py @@ -0,0 +1,290 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Estimator V2 implementation for an arbitrary Backend object.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass + +import numpy as np + +from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.exceptions import QiskitError +from qiskit.providers import BackendV1, BackendV2 +from qiskit.quantum_info import Pauli, PauliList +from qiskit.transpiler import PassManager, PassManagerConfig +from qiskit.transpiler.passes import Optimize1qGatesDecomposition + +from .backend_estimator import _pauli_expval_with_variance, _prepare_counts, _run_circuits +from .base import BaseEstimatorV2 +from .containers import EstimatorPubLike, PrimitiveResult, PubResult +from .containers.bindings_array import BindingsArray +from .containers.estimator_pub import EstimatorPub +from .primitive_job import PrimitiveJob + + +@dataclass +class Options: + """Options for :class:`~.BackendEstimatorV2`.""" + + default_precision: float = 0.015625 + """The default precision to use if none are specified in :meth:`~run`. + Default: 0.015625 (1 / sqrt(4096)). + """ + + abelian_grouping: bool = True + """Whether the observables should be grouped into sets of qubit-wise commuting observables. + Default: True. + """ + + seed_simulator: int | None = None + """The seed to use in the simulator. If None, a random seed will be used. + Default: None. + """ + + +class BackendEstimatorV2(BaseEstimatorV2): + """Evaluates expectation values for provided quantum circuit and observable combinations + + The :class:`~.BackendEstimatorV2` class is a generic implementation of the + :class:`~.BaseEstimatorV2` interface that is used to wrap a :class:`~.BackendV2` + (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimatorV2` API. It + facilitates using backends that do not provide a native + :class:`~.BaseEstimatorV2` implementation in places that work with + :class:`~.BaseEstimatorV2`. However, + if you're using a provider that has a native implementation of + :class:`~.BaseEstimatorV2`, it is a better choice to leverage that native + implementation as it will likely include additional optimizations and be + a more efficient implementation. The generic nature of this class + precludes doing any provider- or backend-specific optimizations. + + This class does not perform any measurement or gate mitigation, and, presently, is only + compatible with Pauli-based observables. + + Each tuple of ``(circuit, observables, parameter values, precision)``, + called an estimator primitive unified bloc (PUB), produces its own array-based result. The + :meth:`~.BackendEstimatorV2.run` method can be given a sequence of pubs to run in one call. + + The options for :class:`~.BackendEstimatorV2` consist of the following items. + + * ``default_precision``: The default precision to use if none are specified in :meth:`~run`. + Default: 0.015625 (1 / sqrt(4096)). + + * ``abelian_grouping``: Whether the observables should be grouped into sets of qubit-wise + commuting observables. + Default: True. + + * ``seed_simulator``: The seed to use in the simulator. If None, a random seed will be used. + Default: None. + """ + + def __init__( + self, + *, + backend: BackendV1 | BackendV2, + options: dict | None = None, + ): + """ + Args: + backend: The backend to run the primitive on. + options: The options to control the default precision (``default_precision``), + the operator grouping (``abelian_grouping``), and + the random seed for the simulator (``seed_simulator``). + """ + self._backend = backend + self._options = Options(**options) if options else Options() + + basis = PassManagerConfig.from_backend(backend).basis_gates + if isinstance(backend, BackendV2): + opt1q = Optimize1qGatesDecomposition(basis=basis, target=backend.target) + else: + opt1q = Optimize1qGatesDecomposition(basis=basis) + self._passmanager = PassManager([opt1q]) + + @property + def options(self) -> Options: + """Return the options""" + return self._options + + @property + def backend(self) -> BackendV1 | BackendV2: + """Returns the backend which this sampler object based on.""" + return self._backend + + def run( + self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None + ) -> PrimitiveJob[PrimitiveResult[PubResult]]: + if precision is None: + precision = self._options.default_precision + coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs] + self._validate_pubs(coerced_pubs) + job = PrimitiveJob(self._run, coerced_pubs) + job._submit() + return job + + def _validate_pubs(self, pubs: list[EstimatorPub]): + for i, pub in enumerate(pubs): + if pub.precision <= 0.0: + raise ValueError( + f"The {i}-th pub has precision less than or equal to 0 ({pub.precision}). ", + "But precision should be larger than 0.", + ) + + def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]: + return PrimitiveResult([self._run_pub(pub) for pub in pubs]) + + def _run_pub(self, pub: EstimatorPub) -> PubResult: + shots = int(np.ceil(1.0 / pub.precision**2)) + circuit = pub.circuit + observables = pub.observables + parameter_values = pub.parameter_values + + # calculate broadcasting of parameters and observables + param_shape = parameter_values.shape + param_indices = np.fromiter(np.ndindex(param_shape), dtype=object).reshape(param_shape) + bc_param_ind, bc_obs = np.broadcast_arrays(param_indices, observables) + + # calculate expectation values for each pair of parameter value set and pauli + param_obs_map = defaultdict(set) + for index in np.ndindex(*bc_param_ind.shape): + param_index = bc_param_ind[index] + param_obs_map[param_index].update(bc_obs[index].keys()) + expval_map = self._calc_expval_paulis(circuit, parameter_values, param_obs_map, shots) + + # calculate expectation values (evs) and standard errors (stds) + evs = np.zeros_like(bc_param_ind, dtype=float) + variances = np.zeros_like(bc_param_ind, dtype=float) + for index in np.ndindex(*bc_param_ind.shape): + param_index = bc_param_ind[index] + for pauli, coeff in bc_obs[index].items(): + expval, variance = expval_map[param_index, pauli] + evs[index] += expval * coeff + variances[index] += variance * coeff**2 + stds = np.sqrt(variances / shots) + data_bin_cls = self._make_data_bin(pub) + data_bin = data_bin_cls(evs=evs, stds=stds) + return PubResult(data_bin, metadata={"target_precision": pub.precision}) + + def _calc_expval_paulis( + self, + circuit: QuantumCircuit, + parameter_values: BindingsArray, + param_obs_map: dict[tuple[int, ...], set[str]], + shots: int, + ) -> dict[tuple[tuple[int, ...], str], tuple[float, float]]: + # generate circuits + circuits = [] + for param_index, pauli_strings in param_obs_map.items(): + bound_circuit = parameter_values.bind(circuit, param_index) + # sort pauli_strings so that the order is deterministic + meas_paulis = PauliList(sorted(pauli_strings)) + new_circuits = self._preprocessing(bound_circuit, meas_paulis, param_index) + circuits.extend(new_circuits) + + # run circuits + result, metadata = _run_circuits( + circuits, self._backend, shots=shots, seed_simulator=self._options.seed_simulator + ) + + # postprocessing results + expval_map: dict[tuple[tuple[int, ...], str], tuple[float, float]] = {} + counts = _prepare_counts(result) + for count, meta in zip(counts, metadata): + orig_paulis = meta["orig_paulis"] + meas_paulis = meta["meas_paulis"] + param_index = meta["param_index"] + expvals, variances = _pauli_expval_with_variance(count, meas_paulis) + for pauli, expval, variance in zip(orig_paulis, expvals, variances): + expval_map[param_index, pauli.to_label()] = (expval, variance) + return expval_map + + def _preprocessing( + self, circuit: QuantumCircuit, observable: PauliList, param_index: tuple[int, ...] + ) -> list[QuantumCircuit]: + # generate measurement circuits with metadata + meas_circuits: list[QuantumCircuit] = [] + if self._options.abelian_grouping: + for obs in observable.group_commuting(qubit_wise=True): + basis = Pauli((np.logical_or.reduce(obs.z), np.logical_or.reduce(obs.x))) + meas_circuit, indices = _measurement_circuit(circuit.num_qubits, basis) + paulis = PauliList.from_symplectic( + obs.z[:, indices], + obs.x[:, indices], + obs.phase, + ) + meas_circuit.metadata = { + "orig_paulis": obs, + "meas_paulis": paulis, + "param_index": param_index, + } + meas_circuits.append(meas_circuit) + else: + for basis in observable: + meas_circuit, indices = _measurement_circuit(circuit.num_qubits, basis) + obs = PauliList(basis) + paulis = PauliList.from_symplectic( + obs.z[:, indices], + obs.x[:, indices], + obs.phase, + ) + meas_circuit.metadata = { + "orig_paulis": obs, + "meas_paulis": paulis, + "param_index": param_index, + } + meas_circuits.append(meas_circuit) + + # unroll basis gates + meas_circuits = self._passmanager.run(meas_circuits) + + # combine measurement circuits + preprocessed_circuits = [] + for meas_circuit in meas_circuits: + circuit_copy = circuit.copy() + # meas_circuit is supposed to have a classical register whose name is different from + # those of the transpiled_circuit + clbits = meas_circuit.cregs[0] + for creg in circuit_copy.cregs: + if clbits.name == creg.name: + raise QiskitError( + "Classical register for measurements conflict with those of the input " + f"circuit: {clbits}. " + "Recommended to avoid register names starting with '__'." + ) + circuit_copy.add_register(clbits) + circuit_copy.compose(meas_circuit, clbits=clbits, inplace=True) + circuit_copy.metadata = meas_circuit.metadata + preprocessed_circuits.append(circuit_copy) + return preprocessed_circuits + + +def _measurement_circuit(num_qubits: int, pauli: Pauli): + # Note: if pauli is I for all qubits, this function generates a circuit to measure only + # the first qubit. + # Although such an operator can be optimized out by interpreting it as a constant (1), + # this optimization requires changes in various methods. So it is left as future work. + qubit_indices = np.arange(pauli.num_qubits)[pauli.z | pauli.x] + if not np.any(qubit_indices): + qubit_indices = [0] + meas_circuit = QuantumCircuit( + QuantumRegister(num_qubits, "q"), ClassicalRegister(len(qubit_indices), f"__c_{pauli}") + ) + for clbit, i in enumerate(qubit_indices): + if pauli.x[i]: + if pauli.z[i]: + meas_circuit.sdg(i) + meas_circuit.h(i) + meas_circuit.measure(i, clbit) + return meas_circuit, qubit_indices diff --git a/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml b/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml new file mode 100644 index 00000000000..c8ac782f4ba --- /dev/null +++ b/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml @@ -0,0 +1,28 @@ +--- +features: + - | + The implementation :class:`~.BackendEstimatorV2` of :class:`~.BaseEstimatorV2` was added. + This estimator supports :class:`~.BackendV1` and :class:`~.BackendV2`. + + .. code-block:: python + + import numpy as np + from qiskit import transpile + from qiskit.circuit.library import IQP + from qiskit.primitives import BackendEstimatorV2 + from qiskit.providers.fake_provider import Fake7QPulseV1 + from qiskit.quantum_info import SparsePauliOp, random_hermitian + + backend = Fake7QPulseV1() + estimator = BackendEstimatorV2(backend=backend) + n_qubits = 5 + mat = np.real(random_hermitian(n_qubits, seed=1234)) + circuit = IQP(mat) + observable = SparsePauliOp("Z" * n_qubits) + isa_circuit = transpile(circuit, backend=backend, optimization_level=1) + isa_observable = observable.apply_layout(isa_circuit.layout) + job = estimator.run([(isa_circuit, isa_observable)], precision=0.01) + result = job.result() + print(f"> Expectation value: {result[0].data.evs}") + print(f"> Standard error: {result[0].data.stds}") + print(f"> Metadata: {result[0].metadata}") diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 6e14d7e4c2d..aab90038267 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -101,7 +101,7 @@ def test_estimator_run(self, backend): # Note that passing objects has an overhead # since the corresponding indices need to be searched. # User can append a circuit and observable. - # calculate [ ] + # calculate [ ] result2 = estimator.run([psi2], [hamiltonian1], [theta2]).result() np.testing.assert_allclose(result2.values, [2.97797666], rtol=0.5, atol=0.2) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py new file mode 100644 index 00000000000..1ed7ce47615 --- /dev/null +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -0,0 +1,409 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. + +"""Tests for Backend Estimator V2.""" + +from __future__ import annotations + +import unittest +from test import QiskitTestCase, combine + +import numpy as np +from ddt import ddt + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import BackendEstimatorV2, StatevectorEstimator +from qiskit.primitives.containers.bindings_array import BindingsArray +from qiskit.primitives.containers.estimator_pub import EstimatorPub +from qiskit.primitives.containers.observables_array import ObservablesArray +from qiskit.providers.backend_compat import BackendV2Converter +from qiskit.providers.basic_provider import BasicSimulator +from qiskit.providers.fake_provider import Fake7QPulseV1 +from qiskit.quantum_info import SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.utils import optionals + +BACKENDS = [BasicSimulator(), Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())] + + +@ddt +class TestBackendEstimatorV2(QiskitTestCase): + """Test Estimator""" + + def setUp(self): + super().setUp() + self._precision = 5e-3 + self._rtol = 3e-1 + self._seed = 12 + self._options = {"default_precision": self._precision, "seed_simulator": self._seed} + self.ansatz = RealAmplitudes(num_qubits=2, reps=2) + self.observable = SparsePauliOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + self.expvals = -1.0284380963435145, -1.284366511861733 + + self.psi = (RealAmplitudes(num_qubits=2, reps=2), RealAmplitudes(num_qubits=2, reps=3)) + self.params = tuple(psi.parameters for psi in self.psi) + self.hamiltonian = ( + SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]), + SparsePauliOp.from_list([("IZ", 1)]), + SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]), + ) + self.theta = ( + [0, 1, 1, 2, 3, 5], + [0, 1, 1, 2, 3, 5, 8, 13], + [1, 2, 3, 4, 5, 6], + ) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_estimator_run(self, backend, abelian_grouping): + """Test Estimator.run()""" + psi1, psi2 = self.psi + hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian + theta1, theta2, theta3 = self.theta + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1, psi2 = pm.run([psi1, psi2]) + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + # Specify the circuit and observable by indices. + # calculate [ ] + ham1 = hamiltonian1.apply_layout(psi1.layout) + job = estimator.run([(psi1, ham1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol) + + # Objects can be passed instead of indices. + # Note that passing objects has an overhead + # since the corresponding indices need to be searched. + # User can append a circuit and observable. + # calculate [ ] + ham1 = hamiltonian1.apply_layout(psi2.layout) + result2 = estimator.run([(psi2, ham1, theta2)]).result() + np.testing.assert_allclose(result2[0].data.evs, [2.97797666], rtol=self._rtol) + + # calculate [ , ] + ham2 = hamiltonian2.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + result3 = estimator.run([(psi1, [ham2, ham3], theta1)]).result() + np.testing.assert_allclose(result3[0].data.evs, [-0.551653, 0.07535239], rtol=self._rtol) + + # calculate [ [, + # ], + # [] ] + ham1 = hamiltonian1.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + ham2 = hamiltonian2.apply_layout(psi2.layout) + result4 = estimator.run( + [ + (psi1, [ham1, ham3], [theta1, theta3]), + (psi2, ham2, theta2), + ] + ).result() + np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) + np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_estimator_with_pub(self, backend, abelian_grouping): + """Test estimator with explicit EstimatorPubs.""" + psi1, psi2 = self.psi + hamiltonian1, hamiltonian2, hamiltonian3 = self.hamiltonian + theta1, theta2, theta3 = self.theta + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1, psi2 = pm.run([psi1, psi2]) + + ham1 = hamiltonian1.apply_layout(psi1.layout) + ham3 = hamiltonian3.apply_layout(psi1.layout) + obs1 = ObservablesArray.coerce([ham1, ham3]) + bind1 = BindingsArray.coerce({tuple(psi1.parameters): [theta1, theta3]}) + pub1 = EstimatorPub(psi1, obs1, bind1) + + ham2 = hamiltonian2.apply_layout(psi2.layout) + obs2 = ObservablesArray.coerce(ham2) + bind2 = BindingsArray.coerce({tuple(psi2.parameters): theta2}) + pub2 = EstimatorPub(psi2, obs2, bind2) + + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + result4 = estimator.run([pub1, pub2]).result() + np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) + np.testing.assert_allclose(result4[1].data.evs, [0.17849238], rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_estimator_run_no_params(self, backend, abelian_grouping): + """test for estimator without parameters""" + circuit = self.ansatz.assign_parameters([0, 1, 1, 2, 3, 5]) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + circuit = pm.run(circuit) + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + observable = self.observable.apply_layout(circuit.layout) + result = est.run([(circuit, observable)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1.284366511861733], rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_run_single_circuit_observable(self, backend, abelian_grouping): + """Test for single circuit and single observable case.""" + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + + with self.subTest("No parameter"): + qc = QuantumCircuit(1) + qc.x(0) + qc = pm.run(qc) + op = SparsePauliOp("Z") + op = op.apply_layout(qc.layout) + param_vals = [None, [], [[]], np.array([]), np.array([[]]), [np.array([])]] + target = [-1] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + with self.subTest("One parameter"): + param = Parameter("x") + qc = QuantumCircuit(1) + qc.ry(param, 0) + qc = pm.run(qc) + op = SparsePauliOp("Z") + op = op.apply_layout(qc.layout) + param_vals = [ + [np.pi], + np.array([np.pi]), + ] + target = [-1] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + with self.subTest("More than one parameter"): + qc = self.psi[0] + qc = pm.run(qc) + op = self.hamiltonian[0] + op = op.apply_layout(qc.layout) + param_vals = [ + self.theta[0], + [self.theta[0]], + np.array(self.theta[0]), + np.array([self.theta[0]]), + [np.array(self.theta[0])], + ] + target = [1.5555572817900956] + for val in param_vals: + self.subTest(f"{val}") + result = est.run([(qc, op, val)]).result() + np.testing.assert_allclose(result[0].data.evs, target, rtol=self._rtol) + self.assertEqual(result[0].metadata["target_precision"], self._precision) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_run_1qubit(self, backend, abelian_grouping): + """Test for 1-qubit cases""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(1) + qc2.x(0) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("Z", 1)]) + + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + op_1 = op.apply_layout(qc.layout) + result = est.run([(qc, op_1)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_2 = op2.apply_layout(qc.layout) + result = est.run([(qc, op_2)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_3 = op.apply_layout(qc2.layout) + result = est.run([(qc2, op_3)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_4 = op2.apply_layout(qc2.layout) + result = est.run([(qc2, op_4)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_run_2qubits(self, backend, abelian_grouping): + """Test for 2-qubit cases (to check endian)""" + qc = QuantumCircuit(2) + qc2 = QuantumCircuit(2) + qc2.x(0) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc, qc2 = pm.run([qc, qc2]) + + op = SparsePauliOp.from_list([("II", 1)]) + op2 = SparsePauliOp.from_list([("ZI", 1)]) + op3 = SparsePauliOp.from_list([("IZ", 1)]) + + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + op_1 = op.apply_layout(qc.layout) + result = est.run([(qc, op_1)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_2 = op.apply_layout(qc2.layout) + result = est.run([(qc2, op_2)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_3 = op2.apply_layout(qc.layout) + result = est.run([(qc, op_3)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_4 = op2.apply_layout(qc2.layout) + result = est.run([(qc2, op_4)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_5 = op3.apply_layout(qc.layout) + result = est.run([(qc, op_5)]).result() + np.testing.assert_allclose(result[0].data.evs, [1], rtol=self._rtol) + + op_6 = op3.apply_layout(qc2.layout) + result = est.run([(qc2, op_6)]).result() + np.testing.assert_allclose(result[0].data.evs, [-1], rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_run_errors(self, backend, abelian_grouping): + """Test for errors""" + qc = QuantumCircuit(1) + qc2 = QuantumCircuit(2) + + op = SparsePauliOp.from_list([("I", 1)]) + op2 = SparsePauliOp.from_list([("II", 1)]) + + est = BackendEstimatorV2(backend=backend, options=self._options) + est.options.abelian_grouping = abelian_grouping + with self.assertRaises(ValueError): + est.run([(qc, op2)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op, [[1e4]])]).result() + with self.assertRaises(ValueError): + est.run([(qc2, op2, [[1, 2]])]).result() + with self.assertRaises(ValueError): + est.run([(qc, [op, op2], [[1]])]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=-1).result() + with self.assertRaises(ValueError): + est.run([(qc, 1j * op)], precision=0.1).result() + # precision == 0 + with self.assertRaises(ValueError): + est.run([(qc, op, None, 0)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=0).result() + # precision < 0 + with self.assertRaises(ValueError): + est.run([(qc, op, None, -1)]).result() + with self.assertRaises(ValueError): + est.run([(qc, op)], precision=-1).result() + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_run_numpy_params(self, backend, abelian_grouping): + """Test for numpy array as parameter values""" + qc = RealAmplitudes(num_qubits=2, reps=2) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) + op = op.apply_layout(qc.layout) + k = 5 + params_array = np.random.rand(k, qc.num_parameters) + params_list = params_array.tolist() + params_list_array = list(params_array) + statevector_estimator = StatevectorEstimator(seed=123) + target = statevector_estimator.run([(qc, op, params_list)]).result() + + backend_estimator = BackendEstimatorV2(backend=backend, options=self._options) + backend_estimator.options.abelian_grouping = abelian_grouping + + with self.subTest("ndarrary"): + result = backend_estimator.run([(qc, op, params_array)]).result() + self.assertEqual(result[0].data.evs.shape, (k,)) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + with self.subTest("list of ndarray"): + result = backend_estimator.run([(qc, op, params_list_array)]).result() + self.assertEqual(result[0].data.evs.shape, (k,)) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + @combine(backend=BACKENDS, abelian_grouping=[True, False]) + def test_precision(self, backend, abelian_grouping): + """Test for precision""" + estimator = BackendEstimatorV2(backend=backend, options=self._options) + estimator.options.abelian_grouping = abelian_grouping + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + psi1 = pm.run(self.psi[0]) + hamiltonian1 = self.hamiltonian[0].apply_layout(psi1.layout) + theta1 = self.theta[0] + job = estimator.run([(psi1, hamiltonian1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) + # The result of the second run is the same + job = estimator.run([(psi1, hamiltonian1, [theta1]), (psi1, hamiltonian1, [theta1])]) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.901141473854881], rtol=self._rtol) + np.testing.assert_allclose(result[1].data.evs, [1.901141473854881], rtol=self._rtol) + # apply smaller precision value + job = estimator.run([(psi1, hamiltonian1, [theta1])], precision=self._precision * 0.5) + result = job.result() + np.testing.assert_allclose(result[0].data.evs, [1.5555572817900956], rtol=self._rtol) + + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") + @combine(abelian_grouping=[True, False]) + def test_aer(self, abelian_grouping): + """Test for Aer simulator""" + from qiskit_aer import AerSimulator + + backend = AerSimulator() + seed = 123 + qc = RealAmplitudes(num_qubits=2, reps=1) + pm = generate_preset_pass_manager(optimization_level=0, backend=backend) + qc = pm.run(qc) + op = [SparsePauliOp("IX"), SparsePauliOp("YI")] + shape = (3, 2) + rng = np.random.default_rng(seed) + params_array = rng.random(shape + (qc.num_parameters,)) + params_list = params_array.tolist() + params_list_array = list(params_array) + statevector_estimator = StatevectorEstimator(seed=seed) + target = statevector_estimator.run([(qc, op, params_list)]).result() + + backend_estimator = BackendEstimatorV2(backend=backend, options=self._options) + backend_estimator.options.abelian_grouping = abelian_grouping + + with self.subTest("ndarrary"): + result = backend_estimator.run([(qc, op, params_array)]).result() + self.assertEqual(result[0].data.evs.shape, shape) + np.testing.assert_allclose( + result[0].data.evs, target[0].data.evs, rtol=self._rtol, atol=1e-1 + ) + + with self.subTest("list of ndarray"): + result = backend_estimator.run([(qc, op, params_list_array)]).result() + self.assertEqual(result[0].data.evs.shape, shape) + np.testing.assert_allclose( + result[0].data.evs, target[0].data.evs, rtol=self._rtol, atol=1e-1 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index e9a8516c2ab..768fb2ef56b 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -75,7 +75,7 @@ def test_estimator_run(self): # Note that passing objects has an overhead # since the corresponding indices need to be searched. # User can append a circuit and observable. - # calculate [ ] + # calculate [ ] result2 = estimator.run([psi2], [hamiltonian1], [theta2]).result() np.testing.assert_allclose(result2.values, [2.97797666]) diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 1c2621acdbf..a46cd0029c7 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -73,7 +73,7 @@ def test_estimator_run(self): # Note that passing objects has an overhead # since the corresponding indices need to be searched. # User can append a circuit and observable. - # calculate [ ] + # calculate [ ] result2 = estimator.run([(psi2, hamiltonian1, theta2)]).result() np.testing.assert_allclose(result2[0].data.evs, [2.97797666])