From fcf531c2e19baae5cb16878e20eb4e9f1049323d Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 11:13:02 -0500 Subject: [PATCH 01/14] Add BaseEstimatorV2 class Taken from 11227, but modified to - remove options from BaseEstimatorV2 - add precision attribute to BaseEstimatorV2 and EstimatorPub - remove BasePrimitiveV2 and BasePub Co-Authored-By: Ikko Hamamura Co-Authored-By: Ian Hincks <2229105+ihincks@users.noreply.github.com> --- qiskit/primitives/__init__.py | 4 +- qiskit/primitives/base/__init__.py | 2 +- qiskit/primitives/base/base_estimator.py | 127 +++++++++++++++-- qiskit/primitives/containers/__init__.py | 1 + qiskit/primitives/containers/estimator_pub.py | 129 ++++++++++++++++++ .../notes/estimatorv2-9b09b66ecc12af1b.yaml | 5 + 6 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 qiskit/primitives/containers/estimator_pub.py create mode 100644 releasenotes/notes/estimatorv2-9b09b66ecc12af1b.yaml diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index 2f54a3346d44..d5d9b223485d 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,6 @@ 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..5bb11a05fa4b 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. # # 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,25 @@ 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 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.estimator_pub import EstimatorPubLike from . import validation +from .base_primitive import BasePrimitive 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 +325,43 @@ 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. + """ + + def __init__(self, precision: float | None): + """ + Args: + precision: a target precision of mean expectation value estimates. + """ + self._precision = precision + + @property + def precision(self) -> float | None: + """The target precision for mean expectation value estimates.""" + return self._precision + + @precision.setter + def precision(self, value: float | None): + self._precision = value + + @abstractmethod + def run(self, pubs: Iterable[EstimatorPubLike]) -> Job: + """Estimate expectation values for each provided pub (Primitive Unified Bloc). + + Args: + pubs: a iterable of pubslike object. Typically, list of tuple + ``(QuantumCircuit, observables, parameter_values)`` + + Returns: + A job object that contains results. + """ + pass diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 0896f4b6bc78..938fb95c222d 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -17,6 +17,7 @@ from .bindings_array import BindingsArray from .bit_array import BitArray from .data_bin import make_data_bin +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/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py new file mode 100644 index 000000000000..417f5944dafd --- /dev/null +++ b/qiskit/primitives/containers/estimator_pub.py @@ -0,0 +1,129 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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 Number + +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 Estimator. + Pub is composed of triple (circuit, observables, parameter_values). + """ + + __slots__ = ("_circuit", "_observables", "_parameter_values", "_shape", "_precision") + + def __init__( + self, + circuit: QuantumCircuit, + observables: ObservablesArray, + parameter_values: BindingsArray | None = None, + precision: float | None = None, + validate: bool = False, + ): + """Initialize an estimator pub. + + Args: + circuit: a quantum circuit. + observables: an observables array. + parameter_values: a bindings array. + precision: a target precision for expectation value estimates. + validate: if True, the input data is validated during initialization. + """ + super().__init__(circuit, validate) + self._observables = observables + self._parameter_values = parameter_values or BindingsArray() + self._precision = precision + + # For ShapedMixin + self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) + + @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: + """A target precision""" + return self._precision + + @classmethod + def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: + """Coerce EstimatorPubLike into EstimatorPub. + + Args: + pub: an object to be estimator pub. + + Returns: + A coerced estimator pub. + """ + if isinstance(pub, EstimatorPub): + return pub + if len(pub) != 2 and len(pub) != 3: + raise ValueError(f"The length of pub must be 2 or 3, but length {len(pub)} is given.") + circuit = pub[0] + observables = ObservablesArray.coerce(pub[1]) + if len(pub) == 2: + return cls(circuit=circuit, observables=observables) + parameter_values = BindingsArray.coerce(pub[2]) + return cls(circuit=circuit, observables=observables, parameter_values=parameter_values) + + def validate(self): + """Validate the pub.""" + super().validate() + self.observables.validate() + self.parameter_values.validate() + # Cross validate circuits and observables + for i, observable in enumerate(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." + ) + if self._precision is not None: + if not isinstance(self._precision, Number): + raise TypeError(f"The target precision must be a float, not {type(self._precision)}") + if self._precision < 0: + raise ValueError(f"The target precision ({self._precision}) must be non-negative") + + +EstimatorPubLike = Union[ + EstimatorPub, Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] +] 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 `_. From ab1c1966b9b06bb54297b75f4fec1c6e47d80054 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 11:40:02 -0500 Subject: [PATCH 02/14] Fix removal of BasePub --- qiskit/primitives/containers/estimator_pub.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 417f5944dafd..bf31769b6908 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -53,7 +53,7 @@ def __init__( precision: a target precision for expectation value estimates. validate: if True, the input data is validated during initialization. """ - super().__init__(circuit, validate) + self._circuit = circuit self._observables = observables self._parameter_values = parameter_values or BindingsArray() self._precision = precision @@ -61,6 +61,9 @@ def __init__( # For ShapedMixin self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) + if validate: + self.validate() + @property def observables(self) -> ObservablesArray: """An observables array""" @@ -99,9 +102,12 @@ def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: def validate(self): """Validate the pub.""" - super().validate() + if not isinstance(self.circuit, QuantumCircuit): + raise TypeError("circuit must be QuantumCircuit.") + self.observables.validate() self.parameter_values.validate() + # Cross validate circuits and observables for i, observable in enumerate(self.observables): num_qubits = len(next(iter(observable))) @@ -110,6 +116,7 @@ def validate(self): 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: @@ -117,6 +124,7 @@ def validate(self): f"The number of values ({num_parameters}) does not match " f"the number of parameters ({self.circuit.num_parameters}) for the circuit." ) + if self._precision is not None: if not isinstance(self._precision, Number): raise TypeError(f"The target precision must be a float, not {type(self._precision)}") From 25018f8765a40089228a46fc7af4ac253878a7ad Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 12:20:13 -0500 Subject: [PATCH 03/14] Remove precision from EstimatorPub --- qiskit/primitives/base/base_estimator.py | 4 ++-- qiskit/primitives/containers/estimator_pub.py | 16 +--------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 5bb11a05fa4b..15e2de8b85a6 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -343,12 +343,12 @@ def __init__(self, precision: float | None): precision: a target precision of mean expectation value estimates. """ self._precision = precision - + @property def precision(self) -> float | None: """The target precision for mean expectation value estimates.""" return self._precision - + @precision.setter def precision(self, value: float | None): self._precision = value diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index bf31769b6908..6519feae0078 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -34,14 +34,13 @@ class EstimatorPub(ShapedMixin): Pub is composed of triple (circuit, observables, parameter_values). """ - __slots__ = ("_circuit", "_observables", "_parameter_values", "_shape", "_precision") + __slots__ = ("_circuit", "_observables", "_parameter_values", "_shape") def __init__( self, circuit: QuantumCircuit, observables: ObservablesArray, parameter_values: BindingsArray | None = None, - precision: float | None = None, validate: bool = False, ): """Initialize an estimator pub. @@ -50,13 +49,11 @@ def __init__( circuit: a quantum circuit. observables: an observables array. parameter_values: a bindings array. - precision: a target precision for expectation value estimates. validate: if True, the input data is validated during initialization. """ self._circuit = circuit self._observables = observables self._parameter_values = parameter_values or BindingsArray() - self._precision = precision # For ShapedMixin self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) @@ -73,11 +70,6 @@ def observables(self) -> ObservablesArray: def parameter_values(self) -> BindingsArray: """A bindings array""" return self._parameter_values - - @property - def precision(self) -> float | None: - """A target precision""" - return self._precision @classmethod def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: @@ -125,12 +117,6 @@ def validate(self): f"the number of parameters ({self.circuit.num_parameters}) for the circuit." ) - if self._precision is not None: - if not isinstance(self._precision, Number): - raise TypeError(f"The target precision must be a float, not {type(self._precision)}") - if self._precision < 0: - raise ValueError(f"The target precision ({self._precision}) must be non-negative") - EstimatorPubLike = Union[ EstimatorPub, Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] From f178fb5cb04e270e03ac0eeb9a65065f95e2d3bf Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 9 Jan 2024 13:56:25 -0500 Subject: [PATCH 04/14] Add BaseEstimator._make_data_bin() static method --- qiskit/primitives/base/base_estimator.py | 11 ++++++++++- qiskit/primitives/containers/__init__.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 15e2de8b85a6..2af93d8cf82c 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -156,6 +156,9 @@ from copy import copy from typing import Generic, TypeVar +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 @@ -163,7 +166,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func -from ..containers.estimator_pub import EstimatorPubLike +from ..containers import make_data_bin, DataBin, EstimatorPub, EstimatorPubLike from . import validation from .base_primitive import BasePrimitive @@ -344,6 +347,12 @@ def __init__(self, precision: float | None): """ self._precision = precision + @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.float]), ("stds", NDArray[np.float])), pub.shape) + @property def precision(self) -> float | None: """The target precision for mean expectation value estimates.""" diff --git a/qiskit/primitives/containers/__init__.py b/qiskit/primitives/containers/__init__.py index 938fb95c222d..4f362c146439 100644 --- a/qiskit/primitives/containers/__init__.py +++ b/qiskit/primitives/containers/__init__.py @@ -16,7 +16,7 @@ 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 3cc901f933e9414f46a2fc893570bdea4cb36306 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 14:19:31 -0500 Subject: [PATCH 05/14] Apply suggestions from code review Co-authored-by: Ian Hincks --- qiskit/primitives/base/base_estimator.py | 11 ++++----- qiskit/primitives/containers/estimator_pub.py | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 2af93d8cf82c..0f0ec1eb9f9e 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -340,10 +340,10 @@ class BaseEstimatorV2: observable combinations. """ - def __init__(self, precision: float | None): + def __init__(self, precision: float): """ Args: - precision: a target precision of mean expectation value estimates. + precision: A target precision for expectation value estimates. """ self._precision = precision @@ -355,7 +355,7 @@ def _make_data_bin(pub: EstimatorPub) -> DataBin: @property def precision(self) -> float | None: - """The target precision for mean expectation value estimates.""" + """The target precision for expectation value estimates.""" return self._precision @precision.setter @@ -366,9 +366,8 @@ def precision(self, value: float | None): def run(self, pubs: Iterable[EstimatorPubLike]) -> Job: """Estimate expectation values for each provided pub (Primitive Unified Bloc). - Args: - pubs: a iterable of pubslike object. Typically, list of tuple - ``(QuantumCircuit, observables, parameter_values)`` + pubs: An iterable of pub-like objects, such as tuples ``(circuit, observables)`` or + ``(circuit, observables, parameter_values)``. Returns: A job object that contains results. diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 6519feae0078..e2d7772f902c 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -30,8 +30,9 @@ class EstimatorPub(ShapedMixin): - """Primitive Unified Bloc for Estimator. - Pub is composed of triple (circuit, observables, parameter_values). + """Primitive Unified Bloc for any Estimator primitive. + + An estimator pub is essentially a triple ``(circuit, observables, parameter_values)``. """ __slots__ = ("_circuit", "_observables", "_parameter_values", "_shape") @@ -46,10 +47,10 @@ def __init__( """Initialize an estimator pub. Args: - circuit: a quantum circuit. - observables: an observables array. - parameter_values: a bindings array. - validate: if True, the input data is validated during initialization. + circuit: A quantum circuit. + observables: An observables array. + parameter_values: A bindings array, if the circuit is parametric. + validate: Whether to validate arguments during initialization. """ self._circuit = circuit self._observables = observables @@ -63,23 +64,23 @@ def __init__( @property def observables(self) -> ObservablesArray: - """An observables array""" + """An observables array.""" return self._observables @property def parameter_values(self) -> BindingsArray: - """A bindings array""" + """A bindings array.""" return self._parameter_values @classmethod def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: - """Coerce EstimatorPubLike into EstimatorPub. + """Coerce :class:`~.EstimatorPubLike` into :class:`~.EstimatorPub`. Args: - pub: an object to be estimator pub. + pub: A compatible object for coersion. Returns: - A coerced estimator pub. + An estimator pub. """ if isinstance(pub, EstimatorPub): return pub From 8a7155b19522a98ef10cfdf861ae36f739e13557 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 14:20:22 -0500 Subject: [PATCH 06/14] Apply suggestions from code review Co-authored-by: Ian Hincks --- qiskit/primitives/base/base_estimator.py | 2 +- qiskit/primitives/containers/estimator_pub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 0f0ec1eb9f9e..214103047792 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, 2023. +# (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 diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index e2d7772f902c..4f05e0583de0 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2023. +# (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 From 2b8bd60408b71a276e448395df9c5f71162ae643 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 9 Jan 2024 14:39:57 -0500 Subject: [PATCH 07/14] linting --- qiskit/primitives/base/base_estimator.py | 2 +- qiskit/primitives/containers/estimator_pub.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 214103047792..a80bd512f920 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -366,7 +366,7 @@ def precision(self, value: float | None): def run(self, pubs: Iterable[EstimatorPubLike]) -> Job: """Estimate expectation values for each provided pub (Primitive Unified Bloc). - pubs: An iterable of pub-like objects, such as tuples ``(circuit, observables)`` or + pubs: An iterable of pub-like objects, such as tuples ``(circuit, observables)`` or ``(circuit, observables, parameter_values)``. Returns: diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 4f05e0583de0..1f0f07293190 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -18,7 +18,6 @@ from __future__ import annotations from typing import Tuple, Union -from numbers import Number import numpy as np @@ -31,7 +30,7 @@ class EstimatorPub(ShapedMixin): """Primitive Unified Bloc for any Estimator primitive. - + An estimator pub is essentially a triple ``(circuit, observables, parameter_values)``. """ @@ -52,6 +51,7 @@ def __init__( parameter_values: A bindings array, if the circuit is parametric. validate: Whether to validate arguments during initialization. """ + super().__init__() self._circuit = circuit self._observables = observables self._parameter_values = parameter_values or BindingsArray() From 2974d879a6ff33b75e10104935c339469e9ea60f Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 10 Jan 2024 12:18:10 -0500 Subject: [PATCH 08/14] Move precision to EstimatorPub and Estimator.run --- qiskit/primitives/base/base_estimator.py | 29 ++++---- qiskit/primitives/containers/estimator_pub.py | 66 +++++++++++++++---- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index a80bd512f920..1078091304ba 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -338,14 +338,11 @@ class BaseEstimatorV2: An estimator estimates expectation values for provided quantum circuit and observable combinations. - """ - def __init__(self, precision: float): - """ - Args: - precision: A target precision for expectation value estimates. - """ - self._precision = precision + 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: @@ -353,21 +350,17 @@ def _make_data_bin(pub: EstimatorPub) -> DataBin: # across implementations return make_data_bin((("evs", NDArray[np.float]), ("stds", NDArray[np.float])), pub.shape) - @property - def precision(self) -> float | None: - """The target precision for expectation value estimates.""" - return self._precision - - @precision.setter - def precision(self, value: float | None): - self._precision = value - @abstractmethod - def run(self, pubs: Iterable[EstimatorPubLike]) -> Job: + def run(self, pubs: Iterable[EstimatorPubLike], precision: float | None = None) -> Job: """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)``. + ``(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. diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 1f0f07293190..351f0f50564d 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -18,6 +18,7 @@ from __future__ import annotations from typing import Tuple, Union +from numbers import Real import numpy as np @@ -31,16 +32,20 @@ class EstimatorPub(ShapedMixin): """Primitive Unified Bloc for any Estimator primitive. - An estimator pub is essentially a triple ``(circuit, observables, parameter_values)``. + 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", "_shape") + __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 = False, ): """Initialize an estimator pub. @@ -49,12 +54,14 @@ def __init__( 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. """ super().__init__() self._circuit = circuit self._observables = observables self._parameter_values = parameter_values or BindingsArray() + self._precision = precision # For ShapedMixin self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) @@ -62,6 +69,11 @@ def __init__( if validate: self.validate() + @property + def circuit(self) -> QuantumCircuit: + """A quantum circuit.""" + return self._circuit + @property def observables(self) -> ObservablesArray: """An observables array.""" @@ -72,26 +84,49 @@ 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) -> EstimatorPub: + def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> EstimatorPub: """Coerce :class:`~.EstimatorPubLike` into :class:`~.EstimatorPub`. Args: - pub: A compatible object for coersion. + 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. """ if isinstance(pub, EstimatorPub): + if pub / precision is None and precision is not None: + cls( + circuit=pub.circuit, + observables=pub.observables, + parameter_values=pub.parameter_values, + precision=precision, + validate=False, + ) return pub - if len(pub) != 2 and len(pub) != 3: - raise ValueError(f"The length of pub must be 2 or 3, but length {len(pub)} is given.") + 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]) - if len(pub) == 2: - return cls(circuit=circuit, observables=observables) - parameter_values = BindingsArray.coerce(pub[2]) - return cls(circuit=circuit, observables=observables, parameter_values=parameter_values) + parameter_values = BindingsArray.coerce(pub[2]) if len(pub) > 1 else None + if len(pub) > 2 and pub[3] is not None: + precision = pub[3] + return cls( + circuit=circuit, + observables=observables, + parameter_values=parameter_values, + precision=precision, + validate=False, + ) def validate(self): """Validate the pub.""" @@ -101,6 +136,12 @@ def validate(self): 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("precisions must be non-negative.") + # Cross validate circuits and observables for i, observable in enumerate(self.observables): num_qubits = len(next(iter(observable))) @@ -120,5 +161,8 @@ def validate(self): EstimatorPubLike = Union[ - EstimatorPub, Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] + EstimatorPub, + Tuple[QuantumCircuit, ObservablesArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike, Real], ] From 9706cdaebf25d43a4689a576deda5224ab31af0d Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Tue, 16 Jan 2024 16:56:26 -0500 Subject: [PATCH 09/14] Update EstimatorV2 run return type, fix some typos --- qiskit/primitives/base/base_estimator.py | 14 ++++++++++++-- qiskit/primitives/containers/estimator_pub.py | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 1078091304ba..51c206d66979 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -166,9 +166,17 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func -from ..containers import make_data_bin, DataBin, EstimatorPub, EstimatorPubLike +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) @@ -351,7 +359,9 @@ def _make_data_bin(pub: EstimatorPub) -> DataBin: return make_data_bin((("evs", NDArray[np.float]), ("stds", NDArray[np.float])), pub.shape) @abstractmethod - def run(self, pubs: Iterable[EstimatorPubLike], precision: float | None = None) -> Job: + def run( + self, pubs: Iterable[EstimatorPubLike], precision: float | None = None + ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: """Estimate expectation values for each provided pub (Primitive Unified Bloc). Args: diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 351f0f50564d..281b1fb5c80c 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -46,7 +46,7 @@ def __init__( observables: ObservablesArray, parameter_values: BindingsArray | None = None, precision: float | None = None, - validate: bool = False, + validate: bool = True, ): """Initialize an estimator pub. @@ -62,10 +62,7 @@ def __init__( self._observables = observables self._parameter_values = parameter_values or BindingsArray() self._precision = precision - - # For ShapedMixin self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) - if validate: self.validate() @@ -101,14 +98,20 @@ def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> Estima 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: + if pub.precision is None and precision is not None: cls( circuit=pub.circuit, observables=pub.observables, parameter_values=pub.parameter_values, precision=precision, - validate=False, + validate=False, # Assume Pub is already validated ) return pub if len(pub) not in [2, 3, 4]: @@ -125,7 +128,7 @@ def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> Estima observables=observables, parameter_values=parameter_values, precision=precision, - validate=False, + validate=True, ) def validate(self): @@ -140,7 +143,7 @@ def validate(self): 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("precisions must be non-negative.") + raise ValueError("precision must be non-negative.") # Cross validate circuits and observables for i, observable in enumerate(self.observables): From eaa7b10bd2f30d86a466c0f99fa26250f66f591d Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 16 Jan 2024 20:31:48 -0500 Subject: [PATCH 10/14] Fix some minor problems --- qiskit/primitives/containers/estimator_pub.py | 24 +++++++++++++++---- .../containers/observables_array.py | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py index 281b1fb5c80c..992c78bba524 100644 --- a/qiskit/primitives/containers/estimator_pub.py +++ b/qiskit/primitives/containers/estimator_pub.py @@ -56,13 +56,27 @@ def __init__( 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 - self._shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) + + # 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() @@ -106,7 +120,7 @@ def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> Estima raise ValueError("precision must be non-negative") if isinstance(pub, EstimatorPub): if pub.precision is None and precision is not None: - cls( + return cls( circuit=pub.circuit, observables=pub.observables, parameter_values=pub.parameter_values, @@ -120,8 +134,8 @@ def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> Estima ) circuit = pub[0] observables = ObservablesArray.coerce(pub[1]) - parameter_values = BindingsArray.coerce(pub[2]) if len(pub) > 1 else None - if len(pub) > 2 and pub[3] is not None: + 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, @@ -146,7 +160,7 @@ def validate(self): raise ValueError("precision must be non-negative.") # Cross validate circuits and observables - for i, observable in enumerate(self.observables): + for i, observable in np.ndenumerate(self.observables): num_qubits = len(next(iter(observable))) if self.circuit.num_qubits != num_qubits: raise ValueError( 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 From bf6f9348b6859ad630459e2abbea5fc5b138118b Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 16 Jan 2024 20:32:18 -0500 Subject: [PATCH 11/14] add tests --- .../containers/test_estimator_pub.py | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 test/python/primitives/containers/test_estimator_pub.py 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) From 9a60b52c5453f42c88f2ca72b962a88046f53451 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 17 Jan 2024 09:28:41 -0500 Subject: [PATCH 12/14] Add `*` arg blocker, np.float -> np.float64 --- qiskit/primitives/__init__.py | 9 ++++++++- qiskit/primitives/base/base_estimator.py | 4 ++-- qiskit/primitives/base/base_sampler.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/qiskit/primitives/__init__.py b/qiskit/primitives/__init__.py index d5d9b223485d..f58761c9ce68 100644 --- a/qiskit/primitives/__init__.py +++ b/qiskit/primitives/__init__.py @@ -61,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, EstimatorPub +from .containers import ( + BindingsArray, + ObservablesArray, + PrimitiveResult, + PubResult, + SamplerPub, + EstimatorPub, +) from .estimator import Estimator from .sampler import Sampler diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 51c206d66979..ad05d050dc5c 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -356,11 +356,11 @@ class BaseEstimatorV2: 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.float]), ("stds", NDArray[np.float])), pub.shape) + 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 + self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None ) -> BasePrimitiveJob[PrimitiveResult[PubResult]]: """Estimate expectation values for each provided pub (Primitive Unified Bloc). 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. From c7dc30a204a30747787ce47ffd6f26a228298bda Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 17 Jan 2024 09:57:44 -0500 Subject: [PATCH 13/14] linting --- qiskit/primitives/base/base_estimator.py | 4 +++- qiskit/primitives/containers/data_bin.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index ad05d050dc5c..e38756fd5b12 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -356,7 +356,9 @@ class BaseEstimatorV2: 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) + return make_data_bin( + (("evs", NDArray[np.float64]), ("stds", NDArray[np.float64])), pub.shape + ) @abstractmethod def run( 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))) From 12c71e58669811c123a815db31f83c4a9296f3f6 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Wed, 17 Jan 2024 13:38:33 -0500 Subject: [PATCH 14/14] add test for line of code changed in observables_array.py --- .../primitives/containers/test_observables_array.py | 10 ++++++++++ 1 file changed, 10 insertions(+) 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()