From a688bf42d541dcb3846c2f5a6436055544568ecb Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Fri, 1 Mar 2024 23:13:03 +0900 Subject: [PATCH 01/13] add BackendEstimatorV2 --- qiskit/primitives/__init__.py | 4 +- qiskit/primitives/backend_estimator.py | 8 +- qiskit/primitives/backend_estimator_v2.py | 258 ++++++++++++ .../primitives/test_backend_estimator_v2.py | 372 ++++++++++++++++++ 4 files changed, 636 insertions(+), 6 deletions(-) create mode 100644 qiskit/primitives/backend_estimator_v2.py create mode 100644 test/python/primitives/test_backend_estimator_v2.py diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index ffa75e1826eb..edcd41f5e5d6 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -387,7 +387,7 @@ Primitives API ============== -Primitives V2 +Estimator V2 ------------- .. autosummary:: @@ -395,6 +395,7 @@ 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 50016dfe7a69..23556e2efe27 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 000000000000..5f103ba6a7ad --- /dev/null +++ b/qiskit/primitives/backend_estimator_v2.py @@ -0,0 +1,258 @@ +# 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 BackendV2 object.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterable + +import numpy as np + +from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.exceptions import QiskitError +from qiskit.providers import BackendV2 +from qiskit.quantum_info import Pauli, PauliList +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + +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 + + +class BackendEstimatorV2(BaseEstimatorV2): + """Evaluates expectation value using Pauli rotation gates. + + The :class:`~.BackendEstimator` class is a generic implementation of the + :class:`~.BaseEstimator` interface that is used to wrap a :class:`~.BackendV2` + (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimator` API. It + facilitates using backends that do not provide a native + :class:`~.BaseEstimator` implementation in places that work with + :class:`~.BaseEstimator`. However, + if you're using a provider that has a native implementation of + :class:`~.BaseEstimator`, 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. + + Implementation of :class:`BaseEstimatorV2` using a backend. + + This class provides a EstimatorV2 interface from any :class:`~.BackendV2` backend + and doesn't do any measurement mitigation, it just computes the expectation values. + At present, this implementation 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:`~.EstimatorV2.run` method can be given a sequence of pubs to run in one call. + """ + + _VARIANCE_UPPER_BOUND: float = 4.0 + + def __init__( + self, + *, + backend: BackendV2, + default_precision: float = 0.0, + abelian_grouping: bool = True, + ): + """ + Args: + backend: Required: the backend to run the primitive on + default_precision: Default precision + abelian_grouping: Whether the observable should be grouped into commuting + """ + super().__init__() + self._backend = backend + self._passmanager = generate_preset_pass_manager(optimization_level=0, backend=backend) + self._default_precision = default_precision + self._abelian_grouping = abelian_grouping + + @property + def default_precision(self) -> float: + """Return the default precision""" + return self._default_precision + + @property + def ablian_grouping(self) -> bool: + """Return whether Abelian grouping is used or not.""" + return self._abelian_grouping + + @property + def backend(self) -> 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._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 0. 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(self._VARIANCE_UPPER_BOUND / 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 and standard deviations + 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={"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) + meas_paulis = PauliList(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) + + # 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._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/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py new file mode 100644 index 000000000000..98c90f7c9906 --- /dev/null +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -0,0 +1,372 @@ +# 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 +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 + +BACKENDS = [BasicSimulator(), BackendV2Converter(Fake7QPulseV1())] + + +@ddt +class TestBackendEstimatorV2(QiskitTestCase): + """Test Estimator""" + + def setUp(self): + super().setUp() + self._precision = 1e-2 + self._rtol = 3e-1 + 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + + # 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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["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["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["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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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() + + @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) + estimator = BackendEstimatorV2( + backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + target = estimator.run([(qc, op, params_list)]).result() + + with self.subTest("ndarrary"): + result = estimator.run([(qc, op, params_array)]).result() + self.assertEqual(len(result[0].data.evs), k) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + with self.subTest("list of ndarray"): + result = estimator.run([(qc, op, params_list_array)]).result() + self.assertEqual(len(result[0].data.evs), 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, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + 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) + + +if __name__ == "__main__": + unittest.main() From 6edfce05b4bc9e9279a9fa5aaf0b28c7e300f70e Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Fri, 1 Mar 2024 23:14:43 +0900 Subject: [PATCH 02/13] reno --- .../notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml 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 000000000000..ce1f8eb5cb62 --- /dev/null +++ b/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + The implementation :class:`~.BackendEstimatorV2` of :class:`~.BaseEstimatorV2` was added. From 49d1ebd89cfc4e2bf0b60895a941351cb019cc24 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Sat, 2 Mar 2024 15:38:52 +0900 Subject: [PATCH 03/13] fix error for aer --- qiskit/primitives/backend_estimator_v2.py | 8 +++-- .../primitives/test_backend_estimator_v2.py | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 5f103ba6a7ad..9c2d6c4b98c7 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -23,7 +23,8 @@ from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2 from qiskit.quantum_info import Pauli, PauliList -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +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 @@ -76,7 +77,10 @@ def __init__( """ super().__init__() self._backend = backend - self._passmanager = generate_preset_pass_manager(optimization_level=0, backend=backend) + basis = PassManagerConfig.from_backend(backend).basis_gates + self._passmanager = PassManager( + [Optimize1qGatesDecomposition(basis=basis, target=backend.target)] + ) self._default_precision = default_precision self._abelian_grouping = abelian_grouping diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 98c90f7c9906..7210c194b78b 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -31,6 +31,7 @@ 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(), BackendV2Converter(Fake7QPulseV1())] @@ -367,6 +368,38 @@ def test_precision(self, backend, abelian_grouping): 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 numpy array as parameter values with Aer""" + from qiskit_aer import AerSimulator + + backend = AerSimulator() + + 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) + estimator = BackendEstimatorV2( + backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + ) + target = estimator.run([(qc, op, params_list)]).result() + + with self.subTest("ndarrary"): + result = estimator.run([(qc, op, params_array)]).result() + self.assertEqual(len(result[0].data.evs), k) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + + with self.subTest("list of ndarray"): + result = estimator.run([(qc, op, params_list_array)]).result() + self.assertEqual(len(result[0].data.evs), k) + np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + if __name__ == "__main__": unittest.main() From eae8fe8e8edbdc4bf9f3f0bba7d5aa3cbdadf202 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Sat, 2 Mar 2024 15:42:19 +0900 Subject: [PATCH 04/13] fix comments of tests --- test/python/primitives/test_backend_estimator.py | 2 +- test/python/primitives/test_backend_estimator_v2.py | 2 +- test/python/primitives/test_estimator.py | 2 +- test/python/primitives/test_statevector_estimator.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 6e14d7e4c2db..aab900382670 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 index 7210c194b78b..674317dc956c 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -92,7 +92,7 @@ def test_estimator_run(self, backend, abelian_grouping): # Note that passing objects has an overhead # since the corresponding indices need to be searched. # User can append a circuit and observable. - # calculate [ ] + # 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) diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index e9a8516c2ab3..768fb2ef56b4 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 1c2621acdbfa..a46cd0029c7f 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]) From 27c920d4cd43538bb5bdaaecc4e24fd13d00de1f Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Sat, 2 Mar 2024 16:07:07 +0900 Subject: [PATCH 05/13] revise aer test --- .../primitives/test_backend_estimator_v2.py | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 674317dc956c..91325fe95f53 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -22,7 +22,7 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import RealAmplitudes -from qiskit.primitives import BackendEstimatorV2 +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 @@ -330,19 +330,21 @@ def test_run_numpy_params(self, backend, abelian_grouping): params_array = np.random.rand(k, qc.num_parameters) params_list = params_array.tolist() params_list_array = list(params_array) - estimator = BackendEstimatorV2( + statevector_estimator = StatevectorEstimator(seed=123) + target = statevector_estimator.run([(qc, op, params_list)]).result() + + backend_estimator = BackendEstimatorV2( backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision ) - target = estimator.run([(qc, op, params_list)]).result() with self.subTest("ndarrary"): - result = estimator.run([(qc, op, params_array)]).result() - self.assertEqual(len(result[0].data.evs), k) + 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 = estimator.run([(qc, op, params_list_array)]).result() - self.assertEqual(len(result[0].data.evs), k) + 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]) @@ -371,34 +373,40 @@ def test_precision(self, backend, abelian_grouping): @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 numpy array as parameter values with Aer""" + """Test for Aer simulator""" from qiskit_aer import AerSimulator backend = AerSimulator() - - qc = RealAmplitudes(num_qubits=2, reps=2) + 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.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) + 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) - estimator = BackendEstimatorV2( + statevector_estimator = StatevectorEstimator(seed=seed) + target = statevector_estimator.run([(qc, op, params_list)]).result() + + backend_estimator = BackendEstimatorV2( backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision ) - target = estimator.run([(qc, op, params_list)]).result() with self.subTest("ndarrary"): - result = estimator.run([(qc, op, params_array)]).result() - self.assertEqual(len(result[0].data.evs), k) - np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + 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 = estimator.run([(qc, op, params_list_array)]).result() - self.assertEqual(len(result[0].data.evs), k) - np.testing.assert_allclose(result[0].data.evs, target[0].data.evs, rtol=self._rtol) + 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__": From e5c09a6b563a535842610b83785dab95e7e942ee Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Sat, 2 Mar 2024 22:59:34 +0900 Subject: [PATCH 06/13] increase presion for test --- test/python/primitives/test_backend_estimator_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 91325fe95f53..05fcf9e8ada9 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -42,7 +42,7 @@ class TestBackendEstimatorV2(QiskitTestCase): def setUp(self): super().setUp() - self._precision = 1e-2 + self._precision = 7e-3 self._rtol = 3e-1 self.ansatz = RealAmplitudes(num_qubits=2, reps=2) self.observable = SparsePauliOp.from_list( From abf87a57e7a2af15e3f613f1f5f7f226afbb64e8 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Tue, 12 Mar 2024 22:50:51 +0900 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: Ian Hincks --- qiskit/primitives/__init__.py | 2 +- qiskit/primitives/backend_estimator_v2.py | 29 +++++++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index edcd41f5e5d6..041255859ff1 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -388,7 +388,7 @@ ============== Estimator V2 -------------- +------------ .. autosummary:: :toctree: ../stubs/ diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 9c2d6c4b98c7..d15e0a71835d 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -35,44 +35,43 @@ class BackendEstimatorV2(BaseEstimatorV2): - """Evaluates expectation value using Pauli rotation gates. + """Evaluates expectation values for provided quantum circuit and observable combinations - The :class:`~.BackendEstimator` class is a generic implementation of the + The :class:`~.BackendEstimatorV2` class is a generic implementation of the :class:`~.BaseEstimator` interface that is used to wrap a :class:`~.BackendV2` - (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimator` API. It + (or :class:`~.BackendV1`) object in the :class:`~.BaseEstimatorV2` API. It facilitates using backends that do not provide a native - :class:`~.BaseEstimator` implementation in places that work with - :class:`~.BaseEstimator`. However, + :class:`~.BaseEstimatorV2` implementation in places that work with + :class:`~.BaseEstimatorV2`. However, if you're using a provider that has a native implementation of - :class:`~.BaseEstimator`, it is a better choice to leverage that native + :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. Implementation of :class:`BaseEstimatorV2` using a backend. - This class provides a EstimatorV2 interface from any :class:`~.BackendV2` backend - and doesn't do any measurement mitigation, it just computes the expectation values. - At present, this implementation is only compatible with Pauli-based observables. + 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:`~.EstimatorV2.run` method can be given a sequence of pubs to run in one call. + :meth:`~.BackendEstimatorV2.run` method can be given a sequence of pubs to run in one call. """ - _VARIANCE_UPPER_BOUND: float = 4.0 + _VARIANCE_UPPER_BOUND: float = 1.0 def __init__( self, *, backend: BackendV2, - default_precision: float = 0.0, + default_precision: float = 0.015, abelian_grouping: bool = True, ): """ Args: - backend: Required: the backend to run the primitive on - default_precision: Default precision + backend: The backend to run the primitive on. + default_precision: The default precision to use if none are specified in :meth:`~run`. abelian_grouping: Whether the observable should be grouped into commuting """ super().__init__() @@ -150,7 +149,7 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: 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={"precision": pub.precision}) + return PubResult(data_bin, metadata={"target_precision": pub.precision}) def _calc_expval_paulis( self, From a0f1628e3098a7e7495daafe15b98d3d2c0ad482 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Tue, 12 Mar 2024 23:11:30 +0900 Subject: [PATCH 08/13] allow BackendV1 --- qiskit/primitives/backend_estimator_v2.py | 31 ++++++++++--------- .../primitives/test_backend_estimator_v2.py | 10 +++--- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index d15e0a71835d..e9e36e70040b 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -10,7 +10,7 @@ # 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 BackendV2 object.""" +"""Estimator V2 implementation for an arbitrary Backend object.""" from __future__ import annotations @@ -21,7 +21,7 @@ from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.exceptions import QiskitError -from qiskit.providers import BackendV2 +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 @@ -51,7 +51,7 @@ class BackendEstimatorV2(BaseEstimatorV2): Implementation of :class:`BaseEstimatorV2` using a backend. - This class does not perform any measurement or gate mitigation, and, presently, is only + 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)``, @@ -59,30 +59,33 @@ class BackendEstimatorV2(BaseEstimatorV2): :meth:`~.BackendEstimatorV2.run` method can be given a sequence of pubs to run in one call. """ - _VARIANCE_UPPER_BOUND: float = 1.0 - def __init__( self, *, - backend: BackendV2, - default_precision: float = 0.015, + backend: BackendV1 | BackendV2, + default_precision: float = 0.015625, abelian_grouping: bool = True, ): """ Args: backend: The backend to run the primitive on. default_precision: The default precision to use if none are specified in :meth:`~run`. - abelian_grouping: Whether the observable should be grouped into commuting + Default: 0.015625 (1 / sqrt(4096)). + abelian_grouping: Whether the observables should be grouped into sets of + qubit-wise commuting observables. """ super().__init__() self._backend = backend - basis = PassManagerConfig.from_backend(backend).basis_gates - self._passmanager = PassManager( - [Optimize1qGatesDecomposition(basis=basis, target=backend.target)] - ) self._default_precision = default_precision self._abelian_grouping = abelian_grouping + 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 default_precision(self) -> float: """Return the default precision""" @@ -94,7 +97,7 @@ def ablian_grouping(self) -> bool: return self._abelian_grouping @property - def backend(self) -> BackendV2: + def backend(self) -> BackendV1 | BackendV2: """Returns the backend which this sampler object based on.""" return self._backend @@ -120,7 +123,7 @@ 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(self._VARIANCE_UPPER_BOUND / pub.precision**2)) + shots = int(np.ceil(1.0 / pub.precision**2)) circuit = pub.circuit observables = pub.observables parameter_values = pub.parameter_values diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 05fcf9e8ada9..d69a9d021fc9 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -33,7 +33,7 @@ from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.utils import optionals -BACKENDS = [BasicSimulator(), BackendV2Converter(Fake7QPulseV1())] +BACKENDS = [BasicSimulator(), Fake7QPulseV1(), BackendV2Converter(Fake7QPulseV1())] @ddt @@ -42,7 +42,7 @@ class TestBackendEstimatorV2(QiskitTestCase): def setUp(self): super().setUp() - self._precision = 7e-3 + self._precision = 5e-3 self._rtol = 3e-1 self.ansatz = RealAmplitudes(num_qubits=2, reps=2) self.observable = SparsePauliOp.from_list( @@ -178,7 +178,7 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): 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["precision"], self._precision) + self.assertEqual(result[0].metadata["target_precision"], self._precision) with self.subTest("One parameter"): param = Parameter("x") @@ -196,7 +196,7 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): 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["precision"], self._precision) + self.assertEqual(result[0].metadata["target_precision"], self._precision) with self.subTest("More than one parameter"): qc = self.psi[0] @@ -215,7 +215,7 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): 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["precision"], self._precision) + self.assertEqual(result[0].metadata["target_precision"], self._precision) @combine(backend=BACKENDS, abelian_grouping=[True, False]) def test_run_1qubit(self, backend, abelian_grouping): From 571c079996b56774582e35201bca9b8aa42985b2 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Wed, 13 Mar 2024 22:48:05 +0900 Subject: [PATCH 09/13] add seed_simulator to constructor --- qiskit/primitives/backend_estimator_v2.py | 10 +++- .../primitives/test_backend_estimator_v2.py | 51 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index e9e36e70040b..dc83bffe50db 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -65,6 +65,7 @@ def __init__( backend: BackendV1 | BackendV2, default_precision: float = 0.015625, abelian_grouping: bool = True, + seed_simulator: int | None = None, ): """ Args: @@ -73,11 +74,14 @@ def __init__( Default: 0.015625 (1 / sqrt(4096)). abelian_grouping: Whether the observables should be grouped into sets of qubit-wise commuting observables. + seed_simulator: The seed to use in the simulator. + If None, a random seed will be used. """ super().__init__() self._backend = backend self._default_precision = default_precision self._abelian_grouping = abelian_grouping + self._seed_simulator = seed_simulator basis = PassManagerConfig.from_backend(backend).basis_gates if isinstance(backend, BackendV2): @@ -165,12 +169,14 @@ def _calc_expval_paulis( circuits = [] for param_index, pauli_strings in param_obs_map.items(): bound_circuit = parameter_values.bind(circuit, param_index) - meas_paulis = PauliList(pauli_strings) + 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) + result, metadata = _run_circuits( + circuits, self._backend, shots=shots, seed_simulator=self._seed_simulator + ) # postprocessing results expval_map: dict[tuple[tuple[int, ...], str], tuple[float, float]] = {} diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index d69a9d021fc9..da697e60b1b4 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -44,6 +44,7 @@ def setUp(self): super().setUp() self._precision = 5e-3 self._rtol = 3e-1 + self._seed = 12 self.ansatz = RealAmplitudes(num_qubits=2, reps=2) self.observable = SparsePauliOp.from_list( [ @@ -78,7 +79,10 @@ def test_estimator_run(self, backend, abelian_grouping): pm = generate_preset_pass_manager(optimization_level=0, backend=backend) psi1, psi2 = pm.run([psi1, psi2]) estimator = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) # Specify the circuit and observable by indices. @@ -139,7 +143,10 @@ def test_estimator_with_pub(self, backend, abelian_grouping): pub2 = EstimatorPub(psi2, obs2, bind2) estimator = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) result4 = estimator.run([pub1, pub2]).result() np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) @@ -152,7 +159,10 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): pm = generate_preset_pass_manager(optimization_level=0, backend=backend) circuit = pm.run(circuit) est = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) observable = self.observable.apply_layout(circuit.layout) result = est.run([(circuit, observable)]).result() @@ -162,7 +172,10 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): def test_run_single_circuit_observable(self, backend, abelian_grouping): """Test for single circuit and single observable case.""" est = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -230,7 +243,10 @@ def test_run_1qubit(self, backend, abelian_grouping): op2 = SparsePauliOp.from_list([("Z", 1)]) est = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -262,7 +278,10 @@ def test_run_2qubits(self, backend, abelian_grouping): op3 = SparsePauliOp.from_list([("IZ", 1)]) est = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -298,7 +317,10 @@ def test_run_errors(self, backend, abelian_grouping): op2 = SparsePauliOp.from_list([("II", 1)]) est = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) with self.assertRaises(ValueError): est.run([(qc, op2)]).result() @@ -334,7 +356,10 @@ def test_run_numpy_params(self, backend, abelian_grouping): target = statevector_estimator.run([(qc, op, params_list)]).result() backend_estimator = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) with self.subTest("ndarrary"): @@ -351,7 +376,10 @@ def test_run_numpy_params(self, backend, abelian_grouping): def test_precision(self, backend, abelian_grouping): """Test for precision""" estimator = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) pm = generate_preset_pass_manager(optimization_level=0, backend=backend) psi1 = pm.run(self.psi[0]) @@ -391,7 +419,10 @@ def test_aer(self, abelian_grouping): target = statevector_estimator.run([(qc, op, params_list)]).result() backend_estimator = BackendEstimatorV2( - backend=backend, abelian_grouping=abelian_grouping, default_precision=self._precision + backend=backend, + abelian_grouping=abelian_grouping, + default_precision=self._precision, + seed_simulator=self._seed, ) with self.subTest("ndarrary"): From dd4d8a6056ca2812569ead70b943fbeb14eae1d2 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Thu, 14 Mar 2024 16:32:17 +0900 Subject: [PATCH 10/13] introduce options --- qiskit/primitives/backend_estimator_v2.py | 41 ++++++++++++------- .../primitives/test_backend_estimator_v2.py | 30 +++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index dc83bffe50db..2f1777821efe 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -25,6 +25,7 @@ from qiskit.quantum_info import Pauli, PauliList from qiskit.transpiler import PassManager, PassManagerConfig from qiskit.transpiler.passes import Optimize1qGatesDecomposition +from qiskit.providers import Options from .backend_estimator import _pauli_expval_with_variance, _prepare_counts, _run_circuits from .base import BaseEstimatorV2 @@ -64,24 +65,28 @@ def __init__( *, backend: BackendV1 | BackendV2, default_precision: float = 0.015625, - abelian_grouping: bool = True, - seed_simulator: int | None = None, + options: dict | None = None, ): """ Args: backend: The backend to run the primitive on. 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 + options: The options. + + .. notes:: + + ``options`` has the following items. + - abelian_grouping: Whether the observables should be grouped into sets of qubit-wise commuting observables. - seed_simulator: The seed to use in the simulator. + - seed_simulator: The seed to use in the simulator. If None, a random seed will be used. """ - super().__init__() self._backend = backend self._default_precision = default_precision - self._abelian_grouping = abelian_grouping - self._seed_simulator = seed_simulator + self._options = self._default_options() + if options is not None: + self._options.update_options(**options) basis = PassManagerConfig.from_backend(backend).basis_gates if isinstance(backend, BackendV2): @@ -90,16 +95,24 @@ def __init__( opt1q = Optimize1qGatesDecomposition(basis=basis) self._passmanager = PassManager([opt1q]) + @classmethod + def _default_options(cls) -> Options: + """Return the default options""" + return Options( + abelian_grouping=True, + seed_simulator=None, + ) + + @property + def options(self) -> Options: + """Return the options""" + return self._options + @property def default_precision(self) -> float: """Return the default precision""" return self._default_precision - @property - def ablian_grouping(self) -> bool: - """Return whether Abelian grouping is used or not.""" - return self._abelian_grouping - @property def backend(self) -> BackendV1 | BackendV2: """Returns the backend which this sampler object based on.""" @@ -175,7 +188,7 @@ def _calc_expval_paulis( # run circuits result, metadata = _run_circuits( - circuits, self._backend, shots=shots, seed_simulator=self._seed_simulator + circuits, self._backend, shots=shots, seed_simulator=self._options.seed_simulator ) # postprocessing results @@ -195,7 +208,7 @@ def _preprocessing( ) -> list[QuantumCircuit]: # generate measurement circuits with metadata meas_circuits: list[QuantumCircuit] = [] - if self._abelian_grouping: + 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) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index da697e60b1b4..61b786e62b2c 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -80,9 +80,8 @@ def test_estimator_run(self, backend, abelian_grouping): psi1, psi2 = pm.run([psi1, psi2]) estimator = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) # Specify the circuit and observable by indices. @@ -144,9 +143,8 @@ def test_estimator_with_pub(self, backend, abelian_grouping): estimator = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) result4 = estimator.run([pub1, pub2]).result() np.testing.assert_allclose(result4[0].data.evs, [1.55555728, -1.08766318], rtol=self._rtol) @@ -160,9 +158,8 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): circuit = pm.run(circuit) est = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) observable = self.observable.apply_layout(circuit.layout) result = est.run([(circuit, observable)]).result() @@ -173,9 +170,8 @@ def test_run_single_circuit_observable(self, backend, abelian_grouping): """Test for single circuit and single observable case.""" est = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) pm = generate_preset_pass_manager(optimization_level=0, backend=backend) @@ -244,9 +240,8 @@ def test_run_1qubit(self, backend, abelian_grouping): est = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -279,9 +274,8 @@ def test_run_2qubits(self, backend, abelian_grouping): est = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) op_1 = op.apply_layout(qc.layout) result = est.run([(qc, op_1)]).result() @@ -318,9 +312,8 @@ def test_run_errors(self, backend, abelian_grouping): est = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) with self.assertRaises(ValueError): est.run([(qc, op2)]).result() @@ -357,9 +350,8 @@ def test_run_numpy_params(self, backend, abelian_grouping): backend_estimator = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) with self.subTest("ndarrary"): @@ -377,9 +369,8 @@ def test_precision(self, backend, abelian_grouping): """Test for precision""" estimator = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) pm = generate_preset_pass_manager(optimization_level=0, backend=backend) psi1 = pm.run(self.psi[0]) @@ -420,9 +411,8 @@ def test_aer(self, abelian_grouping): backend_estimator = BackendEstimatorV2( backend=backend, - abelian_grouping=abelian_grouping, default_precision=self._precision, - seed_simulator=self._seed, + options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, ) with self.subTest("ndarrary"): From aea346025e5f2f749a6a42f87361595d7588cde4 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Thu, 14 Mar 2024 17:00:03 +0900 Subject: [PATCH 11/13] use dataclass for Options --- qiskit/primitives/backend_estimator_v2.py | 40 +++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 2f1777821efe..b079c99c4180 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -16,6 +16,7 @@ from collections import defaultdict from collections.abc import Iterable +from dataclasses import dataclass import numpy as np @@ -25,7 +26,6 @@ from qiskit.quantum_info import Pauli, PauliList from qiskit.transpiler import PassManager, PassManagerConfig from qiskit.transpiler.passes import Optimize1qGatesDecomposition -from qiskit.providers import Options from .backend_estimator import _pauli_expval_with_variance, _prepare_counts, _run_circuits from .base import BaseEstimatorV2 @@ -35,6 +35,21 @@ from .primitive_job import PrimitiveJob +@dataclass +class Options: + """Options for :class:`~.BackendEstimatorV2`.""" + + 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 @@ -72,21 +87,12 @@ def __init__( backend: The backend to run the primitive on. default_precision: The default precision to use if none are specified in :meth:`~run`. Default: 0.015625 (1 / sqrt(4096)). - options: The options. - - .. notes:: - - ``options`` has the following items. - - abelian_grouping: Whether the observables should be grouped into sets of - qubit-wise commuting observables. - - seed_simulator: The seed to use in the simulator. - If None, a random seed will be used. + options: The options to control the operator grouping (``abelian_grouping``) and + the random seed for the simulator (``seed_simulator``). """ self._backend = backend self._default_precision = default_precision - self._options = self._default_options() - if options is not None: - self._options.update_options(**options) + self._options = Options(**options) if options else Options() basis = PassManagerConfig.from_backend(backend).basis_gates if isinstance(backend, BackendV2): @@ -95,14 +101,6 @@ def __init__( opt1q = Optimize1qGatesDecomposition(basis=basis) self._passmanager = PassManager([opt1q]) - @classmethod - def _default_options(cls) -> Options: - """Return the default options""" - return Options( - abelian_grouping=True, - seed_simulator=None, - ) - @property def options(self) -> Options: """Return the options""" From c809af7cc11470070c0787cefb1e9e19a285522d Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Fri, 15 Mar 2024 16:32:20 +0900 Subject: [PATCH 12/13] move default_precision to options --- qiskit/primitives/backend_estimator_v2.py | 27 ++++--- .../primitives/test_backend_estimator_v2.py | 77 +++++++------------ 2 files changed, 39 insertions(+), 65 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index b079c99c4180..93455ba1c118 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -39,6 +39,11 @@ 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. @@ -79,19 +84,16 @@ def __init__( self, *, backend: BackendV1 | BackendV2, - default_precision: float = 0.015625, options: dict | None = None, ): """ Args: backend: The backend to run the primitive on. - default_precision: The default precision to use if none are specified in :meth:`~run`. - Default: 0.015625 (1 / sqrt(4096)). - options: The options to control the operator grouping (``abelian_grouping``) and + 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._default_precision = default_precision self._options = Options(**options) if options else Options() basis = PassManagerConfig.from_backend(backend).basis_gates @@ -106,11 +108,6 @@ def options(self) -> Options: """Return the options""" return self._options - @property - def default_precision(self) -> float: - """Return the default precision""" - return self._default_precision - @property def backend(self) -> BackendV1 | BackendV2: """Returns the backend which this sampler object based on.""" @@ -120,7 +117,7 @@ def run( self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None ) -> PrimitiveJob[PrimitiveResult[PubResult]]: if precision is None: - precision = self._default_precision + 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) @@ -129,9 +126,10 @@ def run( def _validate_pubs(self, pubs: list[EstimatorPub]): for i, pub in enumerate(pubs): - if pub.precision == 0.0: + if pub.precision <= 0.0: raise ValueError( - f"The {i}-th pub has precision 0. But precision should be larger than 0." + 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]: @@ -155,7 +153,7 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: 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 and standard deviations + # 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): @@ -180,6 +178,7 @@ def _calc_expval_paulis( 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) diff --git a/test/python/primitives/test_backend_estimator_v2.py b/test/python/primitives/test_backend_estimator_v2.py index 61b786e62b2c..1ed7ce47615e 100644 --- a/test/python/primitives/test_backend_estimator_v2.py +++ b/test/python/primitives/test_backend_estimator_v2.py @@ -45,6 +45,7 @@ def setUp(self): 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( [ @@ -78,12 +79,8 @@ def test_estimator_run(self, backend, abelian_grouping): 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, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) - + 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) @@ -141,11 +138,8 @@ def test_estimator_with_pub(self, backend, abelian_grouping): bind2 = BindingsArray.coerce({tuple(psi2.parameters): theta2}) pub2 = EstimatorPub(psi2, obs2, bind2) - estimator = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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) @@ -156,11 +150,8 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): 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, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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) @@ -168,11 +159,8 @@ def test_estimator_run_no_params(self, backend, abelian_grouping): @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, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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"): @@ -238,11 +226,8 @@ def test_run_1qubit(self, backend, abelian_grouping): op = SparsePauliOp.from_list([("I", 1)]) op2 = SparsePauliOp.from_list([("Z", 1)]) - est = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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) @@ -272,11 +257,8 @@ def test_run_2qubits(self, backend, abelian_grouping): op2 = SparsePauliOp.from_list([("ZI", 1)]) op3 = SparsePauliOp.from_list([("IZ", 1)]) - est = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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) @@ -310,11 +292,8 @@ def test_run_errors(self, backend, abelian_grouping): op = SparsePauliOp.from_list([("I", 1)]) op2 = SparsePauliOp.from_list([("II", 1)]) - est = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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): @@ -332,6 +311,11 @@ def test_run_errors(self, backend, abelian_grouping): 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): @@ -348,11 +332,8 @@ def test_run_numpy_params(self, backend, abelian_grouping): statevector_estimator = StatevectorEstimator(seed=123) target = statevector_estimator.run([(qc, op, params_list)]).result() - backend_estimator = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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() @@ -367,11 +348,8 @@ def test_run_numpy_params(self, backend, abelian_grouping): @combine(backend=BACKENDS, abelian_grouping=[True, False]) def test_precision(self, backend, abelian_grouping): """Test for precision""" - estimator = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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) @@ -409,11 +387,8 @@ def test_aer(self, abelian_grouping): statevector_estimator = StatevectorEstimator(seed=seed) target = statevector_estimator.run([(qc, op, params_list)]).result() - backend_estimator = BackendEstimatorV2( - backend=backend, - default_precision=self._precision, - options={"abelian_grouping": abelian_grouping, "seed_simulator": self._seed}, - ) + 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() From 6aaa98dbef0b6584e98eeb59f73f655dbf7275f0 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi Date: Fri, 15 Mar 2024 17:37:22 +0900 Subject: [PATCH 13/13] update docstring and reno --- qiskit/primitives/backend_estimator_v2.py | 18 ++++++++++---- ...backend-estimator-v2-26cf14a3612bb81a.yaml | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/qiskit/primitives/backend_estimator_v2.py b/qiskit/primitives/backend_estimator_v2.py index 93455ba1c118..91e45a16e989 100644 --- a/qiskit/primitives/backend_estimator_v2.py +++ b/qiskit/primitives/backend_estimator_v2.py @@ -59,7 +59,7 @@ class BackendEstimatorV2(BaseEstimatorV2): """Evaluates expectation values for provided quantum circuit and observable combinations The :class:`~.BackendEstimatorV2` class is a generic implementation of the - :class:`~.BaseEstimator` interface that is used to wrap a :class:`~.BackendV2` + :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 @@ -70,14 +70,24 @@ class BackendEstimatorV2(BaseEstimatorV2): a more efficient implementation. The generic nature of this class precludes doing any provider- or backend-specific optimizations. - Implementation of :class:`BaseEstimatorV2` using a backend. - 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__( @@ -89,7 +99,7 @@ def __init__( """ Args: backend: The backend to run the primitive on. - options: The options to control the default_precision (``default_precision``), + options: The options to control the default precision (``default_precision``), the operator grouping (``abelian_grouping``), and the random seed for the simulator (``seed_simulator``). """ diff --git a/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml b/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml index ce1f8eb5cb62..c8ac782f4ba6 100644 --- a/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml +++ b/releasenotes/notes/add-backend-estimator-v2-26cf14a3612bb81a.yaml @@ -2,3 +2,27 @@ 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}")