diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 2f54a3346d44..f58761c9ce68 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -31,6 +31,7 @@ BaseEstimator Estimator BackendEstimator + EstimatorPub Sampler ======= @@ -41,6 +42,7 @@ BaseSampler Sampler BackendSampler + SamplerPub Results ======= @@ -59,6 +61,13 @@ from .base import BaseEstimator, BaseSampler from .base.estimator_result import EstimatorResult from .base.sampler_result import SamplerResult -from .containers import BindingsArray, ObservablesArray, PrimitiveResult, PubResult, SamplerPub +from .containers import ( + BindingsArray, + ObservablesArray, + PrimitiveResult, + PubResult, + SamplerPub, + EstimatorPub, +) from .estimator import Estimator from .sampler import Sampler diff --git a/qiskit/primitives/base/__init__.py b/qiskit/primitives/base/__init__.py index 1b537b0dc0ef..5aaa40018721 100644 --- a/qiskit/primitives/base/__init__.py +++ b/qiskit/primitives/base/__init__.py @@ -14,7 +14,7 @@ Abstract base classes for primitives module. """ -from .base_estimator import BaseEstimator from .base_sampler import BaseSampler, BaseSamplerV2 +from .base_estimator import BaseEstimator, BaseEstimatorV2 from .estimator_result import EstimatorResult from .sampler_result import SamplerResult diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 93b497f02049..e38756fd5b12 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023, 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 @@ -14,9 +14,79 @@ .. estimator-desc: -===================== -Overview of Estimator -===================== +======================== +Overview of EstimatorV2 +======================== + +:class:`~BaseEstimatorV2` is a primitive that estimates expectation values for provided quantum +circuit and observable combinations. + +Following construction, an estimator is used by calling its :meth:`~.BaseEstimatorV2.run` method +with a list of pubs (Primitive Unified Blocs). Each pub contains three values that, together, +define a computation unit of work for the estimator to complete: + +* a single :class:`~qiskit.circuit.QuantumCircuit`, possibly parametrized, whose final state we + define as :math:`\psi(\theta)`, + +* one or more observables (specified as any :class:`~.ObservablesArrayLike`, including + :class:`~.Pauli`, :class:`~.SparsePauliOp`, ``str``) that specify which expectation values to + estimate, denoted :math:`H_j`, and + +* a collection parameter value sets to bind the circuit against, :math:`\theta_k`. + +Running an estimator returns a :class:`~qiskit.providers.JobV1` object, where calling +the method :meth:`qiskit.providers.JobV1.result` results in expectation value estimates and metadata +for each pub: + +.. math:: + + \langle\psi(\theta_k)|H_j|\psi(\theta_k)\rangle + +The observables and parameter values portion of a pub can be array-valued with arbitrary dimensions, +where standard broadcasting rules are applied, so that, in turn, the estimated result for each pub +is in general array-valued as well. For more information, please check +`here `_. + +Here is an example of how the estimator is used. + +.. code-block:: python + + from qiskit.primitives.statevector_estimator import Estimator + from qiskit.circuit.library import RealAmplitudes + from qiskit.quantum_info import SparsePauliOp + + psi1 = RealAmplitudes(num_qubits=2, reps=2) + psi2 = RealAmplitudes(num_qubits=2, reps=3) + + H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + H2 = SparsePauliOp.from_list([("IZ", 1)]) + H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) + + theta1 = [0, 1, 1, 2, 3, 5] + theta2 = [0, 1, 1, 2, 3, 5, 8, 13] + theta3 = [1, 2, 3, 4, 5, 6] + + estimator = Estimator() + + # calculate [ ] + job = estimator.run([(psi1, hamiltonian1, [theta1])]) + job_result = job.result() # It will block until the job finishes. + print(f"The primitive-job finished with result {job_result}")) + + # calculate [ [, + # ], + # [] ] + job2 = estimator.run( + [(psi1, [hamiltonian1, hamiltonian3], [theta1, theta3]), (psi2, hamiltonian2, theta2)] + ) + job_result = job2.result() + print(f"The primitive-job finished with result {job_result}") + + +======================== +Overview of EstimatorV1 +======================== Estimator class estimates expectation values of quantum circuits and observables. @@ -82,24 +152,36 @@ import warnings from abc import abstractmethod -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from copy import copy from typing import Generic, TypeVar -from qiskit.utils.deprecation import deprecate_func +import numpy as np +from numpy.typing import NDArray + from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.providers import JobV1 as Job from qiskit.quantum_info.operators import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.utils.deprecation import deprecate_func -from .base_primitive import BasePrimitive +from ..containers import ( + make_data_bin, + DataBin, + EstimatorPub, + EstimatorPubLike, + PrimitiveResult, + PubResult, +) from . import validation +from .base_primitive import BasePrimitive +from .base_primitive_job import BasePrimitiveJob T = TypeVar("T", bound=Job) -class BaseEstimator(BasePrimitive, Generic[T]): +class BaseEstimatorV1(BasePrimitive, Generic[T]): """Estimator base class. Base class for Estimator that estimates expectation values of quantum circuits and observables. @@ -254,3 +336,45 @@ def parameters(self) -> tuple[ParameterView, ...]: Parameters, where ``parameters[i][j]`` is the j-th parameter of the i-th circuit. """ return tuple(self._parameters) + + +BaseEstimator = BaseEstimatorV1 + + +class BaseEstimatorV2: + """Estimator base class version 2. + + An estimator estimates expectation values for provided quantum circuit and + observable combinations. + + An Estimator implementation must treat the :meth:`.run` method ``precision=None`` + kwarg as using a default ``precision`` value. The default value and methods to + set it can be determined by the Estimator implementor. + """ + + @staticmethod + def _make_data_bin(pub: EstimatorPub) -> DataBin: + # provide a standard way to construct estimator databins to ensure that names match + # across implementations + return make_data_bin( + (("evs", NDArray[np.float64]), ("stds", NDArray[np.float64])), pub.shape + ) + + @abstractmethod + def run( + self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None + ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: + """Estimate expectation values for each provided pub (Primitive Unified Bloc). + + Args: + pubs: An iterable of pub-like objects, such as tuples ``(circuit, observables)`` or + ``(circuit, observables, parameter_values)``. + precision: The target precision for expectation value estimates of each + run :class:`.EstimatorPub` that does not specify its own + precision. If None the estimator's default precision value + will be used. + + Returns: + A job object that contains results. + """ + pass diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 9cf315e84fd8..afd9b44d1af5 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -278,7 +278,7 @@ class BaseSamplerV2: @abstractmethod def run( - self, pubs: Iterable[SamplerPubLike], shots: int | None = None + self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: """Run and collect samples from each pub. diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 0896f4b6bc78..4f362c146439 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -16,7 +16,8 @@ from .bindings_array import BindingsArray from .bit_array import BitArray -from .data_bin import make_data_bin +from .data_bin import make_data_bin, DataBin +from .estimator_pub import EstimatorPub, EstimatorPubLike from .observables_array import ObservablesArray from .primitive_result import PrimitiveResult from .pub_result import PubResult diff --git a/qiskit/primitives/containers/data_bin.py b/qiskit/primitives/containers/data_bin.py index b4e479266e72..de2c4f616dd7 100644 --- a/qiskit/primitives/containers/data_bin.py +++ b/qiskit/primitives/containers/data_bin.py @@ -59,7 +59,7 @@ def make_data_bin( .. code-block:: python - my_bin = make_data_bin([("alpha", np.NDArray[np.float])], shape=(20, 30)) + my_bin = make_data_bin([("alpha", np.NDArray[np.float64])], shape=(20, 30)) # behaves like a dataclass my_bin(alpha=np.empty((20, 30))) diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py new file mode 100644 index 000000000000..992c78bba524 --- /dev/null +++ b/qiskit/primitives/containers/estimator_pub.py @@ -0,0 +1,185 @@ +# 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 Pub class +""" + +from __future__ import annotations + +from typing import Tuple, Union +from numbers import Real + +import numpy as np + +from qiskit import QuantumCircuit + +from .bindings_array import BindingsArray, BindingsArrayLike +from .observables_array import ObservablesArray, ObservablesArrayLike +from .shape import ShapedMixin + + +class EstimatorPub(ShapedMixin): + """Primitive Unified Bloc for any Estimator primitive. + + An estimator pub is essentially a tuple ``(circuit, observables, parameter_values, precision)``. + + If precision is provided this should be used for the target precision of an + estimator, if ``precision=None`` the estimator will determine the target precision. + """ + + __slots__ = ("_circuit", "_observables", "_parameter_values", "_precision", "_shape") + + def __init__( + self, + circuit: QuantumCircuit, + observables: ObservablesArray, + parameter_values: BindingsArray | None = None, + precision: float | None = None, + validate: bool = True, + ): + """Initialize an estimator pub. + + Args: + circuit: A quantum circuit. + observables: An observables array. + parameter_values: A bindings array, if the circuit is parametric. + precision: An optional target precision for expectation value estimates. + validate: Whether to validate arguments during initialization. + + Raises: + ValueError: If the ``observables`` and ``parameter_values`` are not broadcastable, that + is, if their shapes, when right-aligned, do not agree or equal 1. + """ + super().__init__() + self._circuit = circuit + self._observables = observables + self._parameter_values = parameter_values or BindingsArray() + self._precision = precision + + # for ShapedMixin + try: + # _shape has to be defined to properly be Shaped, so we can't put it in validation + self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) + except ValueError as ex: + raise ValueError( + f"The observables shape {self.observables.shape} and the " + f"parameter values shape {self.parameter_values.shape} are not broadcastable." + ) from ex + + if validate: + self.validate() + + @property + def circuit(self) -> QuantumCircuit: + """A quantum circuit.""" + return self._circuit + + @property + def observables(self) -> ObservablesArray: + """An observables array.""" + return self._observables + + @property + def parameter_values(self) -> BindingsArray: + """A bindings array.""" + return self._parameter_values + + @property + def precision(self) -> float | None: + """The target precision for expectation value estimates (optional).""" + return self._precision + + @classmethod + def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> EstimatorPub: + """Coerce :class:`~.EstimatorPubLike` into :class:`~.EstimatorPub`. + + Args: + pub: A compatible object for coercion. + precision: an optional default precision to use if not + already specified by the pub-like object. + + Returns: + An estimator pub. + """ + # Validate precision kwarg if provided + if precision is not None: + if not isinstance(precision, Real): + raise TypeError(f"precision must be a real number, not {type(precision)}.") + if precision < 0: + raise ValueError("precision must be non-negative") + if isinstance(pub, EstimatorPub): + if pub.precision is None and precision is not None: + return cls( + circuit=pub.circuit, + observables=pub.observables, + parameter_values=pub.parameter_values, + precision=precision, + validate=False, # Assume Pub is already validated + ) + return pub + if len(pub) not in [2, 3, 4]: + raise ValueError( + f"The length of pub must be 2, 3 or 4, but length {len(pub)} is given." + ) + circuit = pub[0] + observables = ObservablesArray.coerce(pub[1]) + parameter_values = BindingsArray.coerce(pub[2]) if len(pub) > 2 else None + if len(pub) > 3 and pub[3] is not None: + precision = pub[3] + return cls( + circuit=circuit, + observables=observables, + parameter_values=parameter_values, + precision=precision, + validate=True, + ) + + def validate(self): + """Validate the pub.""" + if not isinstance(self.circuit, QuantumCircuit): + raise TypeError("circuit must be QuantumCircuit.") + + self.observables.validate() + self.parameter_values.validate() + + if self.precision is not None: + if not isinstance(self.precision, Real): + raise TypeError(f"precision must be a real number, not {type(self.precision)}.") + if self.precision < 0: + raise ValueError("precision must be non-negative.") + + # Cross validate circuits and observables + for i, observable in np.ndenumerate(self.observables): + num_qubits = len(next(iter(observable))) + if self.circuit.num_qubits != num_qubits: + raise ValueError( + f"The number of qubits of the circuit ({self.circuit.num_qubits}) does " + f"not match the number of qubits of the {i}-th observable ({num_qubits})." + ) + + # Cross validate circuits and parameter_values + num_parameters = self.parameter_values.num_parameters + if num_parameters != self.circuit.num_parameters: + raise ValueError( + f"The number of values ({num_parameters}) does not match " + f"the number of parameters ({self.circuit.num_parameters}) for the circuit." + ) + + +EstimatorPubLike = Union[ + EstimatorPub, + Tuple[QuantumCircuit, ObservablesArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike, Real], +] diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 0d33111d5957..12dd51837b20 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -211,7 +211,7 @@ def coerce(cls, observables: ObservablesArrayLike) -> ObservablesArray: def validate(self): """Validate the consistency in observables array.""" num_qubits = None - for obs in self._array: + for obs in self._array.reshape(-1): basis_num_qubits = len(next(iter(obs))) if num_qubits is None: num_qubits = basis_num_qubits diff --git a/releasenotes/notes/estimatorv2-9b09b66ecc12af1b.yaml b/releasenotes/notes/estimatorv2-9b09b66ecc12af1b.yaml new file mode 100644 index 000000000000..70504ac8f31f --- /dev/null +++ b/releasenotes/notes/estimatorv2-9b09b66ecc12af1b.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add :class:`~.BaseEstimatorV2` primitive base class for EstimatorV2 based on + `the RFC `_. diff --git a/test/python/primitives/containers/test_estimator_pub.py b/test/python/primitives/containers/test_estimator_pub.py new file mode 100644 index 000000000000..559b6bd3c4d4 --- /dev/null +++ b/test/python/primitives/containers/test_estimator_pub.py @@ -0,0 +1,418 @@ +# 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. + +"""Test EstimatorPub class""" + +import ddt +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.primitives.containers import BindingsArray, EstimatorPub, ObservablesArray +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class EstimatorPubTestCase(QiskitTestCase): + """Test the EstimatorPub class.""" + + def test_properties(self): + """Test EstimatorPub properties.""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + parameter_values = BindingsArray(kwvals={params: np.ones((10, 2))}) + observables = ObservablesArray([{"XX": 0.1}]) + precision = 0.05 + + pub = EstimatorPub( + circuit=circuit, + observables=observables, + parameter_values=parameter_values, + precision=precision, + ) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual( + pub.observables, + observables, + msg="incorrect value for `observables` property", + ) + self.assertEqual( + pub.parameter_values, + parameter_values, + msg="incorrect value for `parameter_values` property", + ) + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + + def test_invalidate_circuit(self): + """Test validation of circuit argument""" + # Invalid circuit, it is an instruction + circuit = QuantumCircuit(3).to_instruction() + obs = ObservablesArray([{"XYZ": 1}]) + with self.assertRaisesRegex(TypeError, "must be QuantumCircuit"): + EstimatorPub(circuit, obs) + + @ddt.data("a", (1.0,)) + def test_invalidate_precision_type(self, precision): + """Test validation of precision argument type""" + obs = ObservablesArray([{"XYZ": 1}]) + with self.assertRaisesRegex(TypeError, "must be a real number"): + EstimatorPub(QuantumCircuit(3), obs, precision=precision) + + def test_invalidate_precision_value(self): + """Test invalid precision argument value""" + obs = ObservablesArray([{"XYZ": 1}]) + with self.assertRaisesRegex(ValueError, "non-negative"): + EstimatorPub(QuantumCircuit(3), obs, precision=-1) + + @ddt.idata(range(5)) + def test_validate_no_parameters(self, num_params): + """Test unparameterized circuit raises for parameter values""" + circuit = QuantumCircuit(2) + obs = ObservablesArray([{"XY": 1}]) + parameter_values = BindingsArray(np.zeros((2, num_params)), shape=2) + if num_params == 0: + EstimatorPub(circuit, obs, parameter_values=parameter_values) + return + + with self.assertRaisesRegex(ValueError, rf"number.+\({num_params}\).+not match.+\(0\)"): + EstimatorPub(circuit, obs, parameter_values=parameter_values) + + def test_validate_num_qubits(self): + """Test unparameterized circuit raises for parameter values""" + circuit = QuantumCircuit(2) + EstimatorPub(circuit, ObservablesArray([{"XY": 1}])) + + with self.assertRaisesRegex(ValueError, r"qubits .+ \(2\) does not match .+ \(3\)"): + EstimatorPub(circuit, ObservablesArray([{"XYZ": 1}])) + + @ddt.idata(range(5)) + def test_validate_num_parameters(self, num_params): + """Test unparameterized circuit raises for parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + + obs = ObservablesArray([{"XY": 1}]) + parameter_values = BindingsArray(np.zeros((2, num_params)), shape=2) + + if num_params == len(params): + EstimatorPub(circuit, obs, parameter_values=parameter_values) + return + + with self.assertRaisesRegex(ValueError, "does not match"): + EstimatorPub(circuit, obs, parameter_values=parameter_values) + + @ddt.data((), (3,), (2, 3)) + def test_shaped_zero_parameter_values(self, shape): + """Test Passing in a shaped array with no parameters works""" + circuit = QuantumCircuit(2) + obs = ObservablesArray({"XZ": 1}) + parameter_values = BindingsArray(np.zeros((*shape, 0)), shape=shape) + pub = EstimatorPub(circuit, obs, parameter_values=parameter_values) + self.assertEqual(pub.shape, shape) + + def test_coerce_circuit(self): + """Test coercing an unparameterized circuit""" + circuit = QuantumCircuit(10) + + obs = ObservablesArray({"XYZXYZXYZX": 1}) + + pub = EstimatorPub.coerce((circuit, obs)) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.observables, obs, msg="incorrect value for `observables` property") + self.assertEqual(pub.precision, None, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + def test_invalid_coerce_circuit(self): + """Test coercing parameterized circuit raises""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(10) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + + obs = ObservablesArray({"XYZXYZXYZX": 1}) + + with self.assertRaises(ValueError): + EstimatorPub.coerce((circuit, obs)) + + @ddt.data(0.01, 0.02) + def test_coerce_pub_with_precision(self, precision): + """Test coercing an EstimatorPub""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + obs = ObservablesArray({"XY": 1}) + pub1 = EstimatorPub( + circuit, + obs, + parameter_values=BindingsArray(kwvals={params: np.ones((10, 2))}), + precision=0.01, + ) + pub2 = EstimatorPub.coerce(pub1, precision=precision) + self.assertEqual(pub1, pub2) + + @ddt.data(0.01, 0.02) + def test_coerce_pub_without_shots(self, precision): + """Test coercing an EstimatorPub""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + obs = ObservablesArray({"XY": 1}) + pub1 = EstimatorPub( + circuit, + obs, + parameter_values=BindingsArray(kwvals={params: np.ones((10, 2))}), + precision=None, + ) + pub2 = EstimatorPub.coerce(pub1, precision=precision) + self.assertEqual(pub1.circuit, pub2.circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub1.observables, pub2.observables) + self.assertEqual( + pub1.parameter_values, + pub2.parameter_values, + msg="incorrect value for `parameter_values` property", + ) + self.assertEqual(pub2.precision, precision, msg="incorrect value for `precision` property") + + @ddt.data(None, 0.08) + def test_coerce_tuple_1(self, precision): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + obs = ObservablesArray({"XY": 1}) + pub = EstimatorPub.coerce((circuit, obs), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.observables, obs, msg="incorrect value for `observables` property") + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_2(self, precision): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + obs = ObservablesArray({"XY": 1}) + parameter_values = np.zeros((4, 3, 2)) + pub = EstimatorPub.coerce((circuit, obs, parameter_values), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.observables, obs, msg="incorrect value for `observables` property") + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_2_trivial_params(self, precision): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + obs = ObservablesArray({"ZZ": 1}) + pub = EstimatorPub.coerce((circuit, obs, None), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.observables, obs, msg="incorrect value for `observables` property") + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 0.08) + def test_coerce_tuple_3(self, precision): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + obs = ObservablesArray({"XY": 1}) + parameter_values = np.zeros((4, 3, 2)) + pub = EstimatorPub.coerce((circuit, obs, parameter_values, 0.08), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.precision, 0.08, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 0.07) + def test_coerce_tuple_3_trivial_shots(self, precision): + """Test coercing circuit and parameter values""" + params = (Parameter("a"), Parameter("b")) + circuit = QuantumCircuit(2) + circuit.rx(params[0], 0) + circuit.ry(params[1], 1) + obs = ObservablesArray({"XY": 1}) + parameter_values = np.zeros((4, 3, 2)) + pub = EstimatorPub.coerce((circuit, obs, parameter_values, None), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, + (4, 3), + msg="incorrect shape for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 2, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data(None, 1, 100) + def test_coerce_tuple_3_trivial_params_shots(self, precision): + """Test coercing circuit and parameter values""" + circuit = QuantumCircuit(2) + obs = ObservablesArray({"XY": 1}) + pub = EstimatorPub.coerce((circuit, obs, None, None), precision=precision) + self.assertEqual(pub.circuit, circuit, msg="incorrect value for `circuit` property") + self.assertEqual(pub.precision, precision, msg="incorrect value for `precision` property") + # Check bindings array, this is more cumbersome since the class doesn't have an eq method + self.assertIsInstance( + pub.parameter_values, + BindingsArray, + msg="incorrect type for `parameter_values` property", + ) + self.assertEqual( + pub.parameter_values.shape, (), msg="incorrect shape for `parameter_values` property" + ) + self.assertEqual( + pub.parameter_values.num_parameters, + 0, + msg="incorrect num parameters for `parameter_values` property", + ) + + @ddt.data( + [(), (), ()], + [(5,), (5,), (5,)], + [(1,), (5,), (5,)], + [(5,), (1,), (5,)], + [(), (5,), (5,)], + [(5,), (), (5,)], + [(3, 4, 5), (3, 4, 5), (3, 4, 5)], + [(2, 1, 10), (4, 1), (2, 4, 10)], + ) + @ddt.unpack + def test_broadcasting(self, obs_shape, params_shape, pub_shape): + """Test that we end up with the correct broadcasted shape.""" + # sanity check that we agree with the NumPy convention + self.assertEqual(np.broadcast_shapes(obs_shape, params_shape), pub_shape) + + params = list(map(Parameter, "abcdef")) + circuit = QuantumCircuit(2) + for idx in range(3): + circuit.rz(params[2 * idx], 0) + circuit.rz(params[2 * idx + 1], 1) + + obs = ObservablesArray([{"XX": 1}] * np.prod(obs_shape, dtype=int)).reshape(obs_shape) + params = BindingsArray(np.empty(params_shape + (6,))) + + pub = EstimatorPub(circuit, obs, params) + self.assertEqual(obs.shape, obs_shape) + self.assertEqual(params.shape, params_shape) + self.assertEqual(pub.shape, pub_shape) + + @ddt.data( + [(5,), (6,)], + [(3,), (5,)], + [(3, 8, 5), (3, 4, 5)], + [(1, 1, 10), (4, 11)], + ) + @ddt.unpack + def test_broadcasting_fails(self, obs_shape, params_shape): + """Test that we get the right error if the entries are not broadcastable.""" + # sanity check that we agree with the NumPy convention + with self.assertRaises(ValueError): + np.broadcast_shapes(obs_shape, params_shape) + + params = list(map(Parameter, "abcdef")) + circuit = QuantumCircuit(2) + for idx in range(3): + circuit.rz(params[2 * idx], 0) + circuit.rz(params[2 * idx + 1], 1) + + obs = ObservablesArray([{"XX": 1}] * np.prod(obs_shape, dtype=int)).reshape(obs_shape) + params = BindingsArray(np.empty(params_shape + (6,))) + self.assertEqual(obs.shape, obs_shape) + self.assertEqual(params.shape, params_shape) + + msg = rf"observables shape \({obs_shape}\) .+ values shape \({params_shape}\) are not" + with self.assertRaisesRegex(ValueError, msg): + EstimatorPub(circuit, obs, params) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index f3b08cec9229..fd43ebe09db5 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -304,3 +304,13 @@ def test_reshape(self): self.assertEqual( obs_rs[idx], {labels_rs[idx]: 1}, msg=f"failed for shape {shape}" ) + + def test_validate(self): + """Test the validate method""" + ObservablesArray({"XX": 1}).validate() + ObservablesArray([{"XX": 1}] * 5).validate() + ObservablesArray([{"XX": 1}] * 15).reshape((3, 5)).validate() + + obs = ObservablesArray([{"XX": 1}, {"XYZ": 1}], validate=False) + with self.assertRaisesRegex(ValueError, "number of qubits must be the same"): + obs.validate()