From 5c2aa096acb8e90e946c9dcde2ca0493b3bdf0bf Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 17 Jan 2024 16:27:43 -0500 Subject: [PATCH 01/19] WIP EstimatorV2 --- qiskit_ibm_runtime/base_primitive.py | 5 + qiskit_ibm_runtime/estimator.py | 23 ++- .../qiskit/primitives/base_estimator.py | 127 ++++++------- .../qiskit/primitives/base_pub.py | 30 ++-- .../qiskit/primitives/estimator_pub.py | 170 +++++++++++++----- .../qiskit/primitives/observables_array.py | 32 +++- .../qiskit/primitives/sampler_pub.py | 138 ++++++++++---- 7 files changed, 370 insertions(+), 155 deletions(-) diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index ca93ebfde..48e6ded9f 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -170,6 +170,11 @@ def session(self) -> Optional[Session]: """ return self._session + @property + def options(self) -> BaseOptions: + """Return options""" + return self._options + def _set_options(self, options: Optional[Union[Dict, BaseOptions]] = None) -> None: """Set options.""" if options is None: diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index a0ce8a29f..356d85914 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -14,7 +14,7 @@ from __future__ import annotations import os -from typing import Optional, Dict, Sequence, Any, Union +from typing import Optional, Dict, Sequence, Any, Union, Iterable import logging import typing @@ -29,8 +29,10 @@ from .base_primitive import BasePrimitiveV1, BasePrimitiveV2 from .utils.qctrl import validate as qctrl_validate -# TODO: remove when we have real v2 base estimator + +# TODO: remove when we have real v2 base estimator, estimatorpub, and estimatorpublike from .qiskit.primitives import BaseEstimatorV2 +from .qiskit.primitives.estimator_pub import EstimatorPub, EstimatorPubLike # pylint: disable=unused-import,cyclic-import from .session import Session @@ -130,6 +132,23 @@ def __init__( if self._service._channel_strategy == "q-ctrl": raise NotImplementedError("EstimatorV2 is not supported with q-ctrl channel strategy.") + def run( + self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike], precision: float | None = None + ) -> RuntimeJob: + """TODO: docstring""" + if isinstance(pubs, EstimatorPub): + pubs = [pubs] + elif isinstance(pubs, tuple) and isinstance(pubs[0], QuantumCircuit): + pubs = [EstimatorPub.coerce(pubs)] + elif pubs is not EstimatorPub: + pubs = [EstimatorPub.coerce(pub) for pub in pubs] + + for pub in pubs: + pub.validate() + + print(pubs[0], type(pubs[0])) + return self._run(pubs) + def _validate_options(self, options: dict) -> None: """Validate that program inputs (options) are valid diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py index 3da9630e0..2ddc9bf24 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py +++ b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py @@ -15,47 +15,43 @@ .. estimator-desc: -===================== -Overview of Estimator -===================== - -Estimator class estimates expectation values of quantum circuits and observables. - -An estimator is initialized with an empty parameter set. The estimator is used to -create a :class:`~qiskit.providers.JobV1`, via the -:meth:`qiskit.primitives.Estimator.run()` method. This method is called -with the following parameters - -* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits - (a list of :class:`~qiskit.circuit.QuantumCircuit` objects). - -* observables (:math:`H_j`): a list of :class:`~qiskit.quantum_info.SparsePauliOp` - objects. - -* parameter values (:math:`\theta_k`): list of sets of values - to be bound to the parameters of the quantum circuits - (list of list of float). - -The method returns a :class:`~qiskit.providers.JobV1` object, calling -:meth:`qiskit.providers.JobV1.result()` yields the -a list of expectation values plus optional metadata like confidence intervals for -the estimation. +======================== +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_i(\theta_k)|H_j|\psi_i(\theta_k)\rangle + \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 import Estimator + 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)]) @@ -63,18 +59,19 @@ 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], [H1], [theta1]) + 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, psi2, psi1], [H1, H2, H3], [theta1, theta2, theta3]) + 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}") """ @@ -82,38 +79,48 @@ from __future__ import annotations from abc import abstractmethod -from typing import Generic, TypeVar, Iterable, Optional +from typing import TypeVar, Iterable -from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job +import numpy as np +from numpy.typing import NDArray + from .estimator_pub import EstimatorPub, EstimatorPubLike -from .base_primitive import BasePrimitiveV2 -from .options import BasePrimitiveOptionsLike T = TypeVar("T", bound=Job) # pylint: disable=invalid-name -class BaseEstimatorV2(BasePrimitiveV2, Generic[T]): - """TODO""" - - def __init__(self, options: Optional[BasePrimitiveOptionsLike] = None): - super().__init__(options=options) - - def run(self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike]) -> T: - """TODO: docstring""" - if isinstance(pubs, EstimatorPub): - pubs = [pubs] - elif isinstance(pubs, tuple) and isinstance(pubs[0], QuantumCircuit): - pubs = [EstimatorPub.coerce(pubs)] - elif pubs is not EstimatorPub: - pubs = [EstimatorPub.coerce(pub) for pub in pubs] - - for pub in pubs: - pub.validate() +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. + """ - return self._run(pubs) + @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: list[EstimatorPub]) -> T: + 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_ibm_runtime/qiskit/primitives/base_pub.py b/qiskit_ibm_runtime/qiskit/primitives/base_pub.py index da2d31d95..238463ade 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/base_pub.py +++ b/qiskit_ibm_runtime/qiskit/primitives/base_pub.py @@ -11,27 +11,37 @@ # that they have been altered from the originals. """ -Base Pub class +Base Pubs class """ from __future__ import annotations -from dataclasses import dataclass - from qiskit import QuantumCircuit -@dataclass(frozen=True) class BasePub: - """Base class for Pub""" + """Base class for PUB (Primitive Unified Bloc)""" - circuit: QuantumCircuit + __slots__ = ("_circuit",) - def validate(self) -> None: - """Validate the inputs. + def __init__(self, circuit: QuantumCircuit, validate: bool = False): + """ + Initialize a BasePub. - Raises: - TypeError: If input values has an invalid type. + Args: + circuit: Quantum circuit object for the pubs. + validate: if True, the input data is validated during initialization. """ + self._circuit = circuit + if validate: + self.validate() + + @property + def circuit(self) -> QuantumCircuit: + """A quantum circuit for the pub""" + return self._circuit + + def validate(self): + """Validate the data""" if not isinstance(self.circuit, QuantumCircuit): raise TypeError("circuit must be QuantumCircuit.") diff --git a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py b/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py index 3fa517fb7..992c78bba 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py +++ b/qiskit_ibm_runtime/qiskit/primitives/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 @@ -9,81 +9,166 @@ # 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. -# type: ignore + """ -Estiamtor Pub class +Estimator Pub class """ from __future__ import annotations -from dataclasses import dataclass, field -from typing import Union, Tuple +from typing import Tuple, Union +from numbers import Real import numpy as np from qiskit import QuantumCircuit -from .base_pub import BasePub from .bindings_array import BindingsArray, BindingsArrayLike from .observables_array import ObservablesArray, ObservablesArrayLike from .shape import ShapedMixin -@dataclass(frozen=True) -class EstimatorPub(BasePub, ShapedMixin): - """Pub for Estimator. - Pub is composed of triple (circuit, observables, parameter_values). +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. """ - observables: ObservablesArray - parameter_values: BindingsArray = BindingsArray([], shape=()) - _shape: tuple[int, ...] = field(init=False) + __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 - def __post_init__(self): - shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) - super().__setattr__("_shape", 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: + """The target precision for expectation value estimates (optional).""" + return self._precision @classmethod - def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: - """Coerce EstimatorPubLike into EstimatorPub. + def coerce(cls, pub: EstimatorPubLike, precision: float | None = None) -> EstimatorPub: + """Coerce :class:`~.EstimatorPubLike` into :class:`~.EstimatorPub`. Args: - pub: an object to be estimator pub. + pub: A compatible object for coercion. + precision: an optional default precision to use if not + already specified by the pub-like object. Returns: - A coerced estiamtor pub. - - Raises: - ValueError: If input values are invalid. + 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) != 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]) - parameter_values = ( - BindingsArray.coerce(pub[2]) if len(pub) == 3 else BindingsArray([], shape=(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, ) - return cls(circuit=circuit, observables=observables, parameter_values=parameter_values) - def validate(self) -> None: + 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))) - num_qubits = len(next(iter(self.observables.ravel()[0].keys()))) - 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 observable ({num_qubits})." - ) - # Cross validate circuits and paramter_values + 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( @@ -93,5 +178,8 @@ def validate(self) -> None: EstimatorPubLike = Union[ - EstimatorPub, Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike] + EstimatorPub, + Tuple[QuantumCircuit, ObservablesArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike], + Tuple[QuantumCircuit, ObservablesArrayLike, BindingsArrayLike, Real], ] diff --git a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py b/qiskit_ibm_runtime/qiskit/primitives/observables_array.py index 07c36fc63..12dd51837 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py +++ b/qiskit_ibm_runtime/qiskit/primitives/observables_array.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 @@ -9,7 +9,7 @@ # 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. -# type: ignore + """ ND-Array container class for Estimator observables. @@ -20,7 +20,7 @@ from collections import defaultdict from collections.abc import Mapping as MappingType from functools import lru_cache -from typing import Iterable, Mapping, Union +from typing import Iterable, Mapping, Union, overload import numpy as np from numpy.typing import ArrayLike @@ -46,12 +46,13 @@ class ObservablesArray(ShapedMixin): """An ND-array of :const:`.BasisObservable` for an :class:`.Estimator` primitive.""" + __slots__ = ("_array", "_shape") ALLOWED_BASIS: str = "IXYZ01+-lr" """The allowed characters in :const:`BasisObservable` strings.""" def __init__( self, - observables: Union[BasisObservableLike, ArrayLike], + observables: BasisObservableLike | ArrayLike, copy: bool = True, validate: bool = True, ): @@ -104,13 +105,21 @@ def __array__(self, dtype=None): return self._array raise ValueError("Type must be 'None' or 'object'") - def __getitem__(self, args) -> Union[ObservablesArray, BasisObservable]: + @overload + def __getitem__(self, args: int | tuple[int, ...]) -> BasisObservable: + ... + + @overload + def __getitem__(self, args: slice) -> ObservablesArray: + ... + + def __getitem__(self, args): item = self._array[args] if not isinstance(item, np.ndarray): return item return ObservablesArray(item, copy=False, validate=False) - def reshape(self, shape: Union[int, Iterable[int]]) -> "ObservablesArray": + def reshape(self, shape: int | Iterable[int]) -> ObservablesArray: """Return a new array with a different shape. This results in a new view of the same arrays. @@ -201,7 +210,16 @@ def coerce(cls, observables: ObservablesArrayLike) -> ObservablesArray: def validate(self): """Validate the consistency in observables array.""" - pass + num_qubits = None + for obs in self._array.reshape(-1): + basis_num_qubits = len(next(iter(obs))) + if num_qubits is None: + num_qubits = basis_num_qubits + elif basis_num_qubits != num_qubits: + raise ValueError( + "The number of qubits must be the same for all observables in the " + "observables array." + ) @classmethod def _validate_basis(cls, basis: str) -> None: diff --git a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py b/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py index f82d4ed3d..76d5a68e6 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py +++ b/qiskit_ibm_runtime/qiskit/primitives/sampler_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 @@ -9,68 +9,130 @@ # 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. -# type: ignore + """ -Sampler PUB class +Sampler Pub class """ from __future__ import annotations -from dataclasses import dataclass, field -from typing import Union, Optional, Tuple +from typing import Tuple, Union +from numbers import Integral from qiskit import QuantumCircuit -from .base_pub import BasePub from .bindings_array import BindingsArray, BindingsArrayLike from .shape import ShapedMixin -@dataclass(frozen=True) -class SamplerPub(BasePub, ShapedMixin): - """PUB for Sampler. - PUB is composed of double (circuit, parameter_values). +class SamplerPub(ShapedMixin): + """Pub (Primitive Unified Bloc) for Sampler. + + Pub is composed of tuple (circuit, parameter_values, shots). + + If shots are provided this number of shots will be run with the sampler, + if ``shots=None`` the number of run shots is determined by the sampler. """ - parameter_values: Optional[BindingsArray] = BindingsArray([], shape=()) - _shape: tuple[int, ...] = field(init=False) + def __init__( + self, + circuit: QuantumCircuit, + parameter_values: BindingsArray | None = None, + shots: int | None = None, + validate: bool = True, + ): + """Initialize a sampler pub. - def __post_init__(self): - super().__setattr__("_shape", self.parameter_values.shape) + Args: + circuit: A quantum circuit. + parameter_values: A bindings array. + shots: A specific number of shots to run with. This value takes + precedence over any value owed by or supplied to a sampler. + validate: If ``True``, the input data is validated during initialization. + """ + super().__init__() + self._circuit = circuit + self._parameter_values = parameter_values or BindingsArray() + self._shots = shots + self._shape = self._parameter_values.shape + if validate: + self.validate() + + @property + def circuit(self) -> QuantumCircuit: + """A quantum circuit.""" + return self._circuit + + @property + def parameter_values(self) -> BindingsArray: + """A bindings array.""" + return self._parameter_values + + @property + def shots(self) -> int | None: + """An specific number of shots to run with (optional). + + This value takes precedence over any value owed by or supplied to a sampler. + """ + return self._shots @classmethod - def coerce(cls, pub: SamplerPubLike) -> SamplerPub: - """Coerce SamplerPubLike into SamplerPub. + def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: + """Coerce a :class:`~.SamplerPubLike` object into a :class:`~.SamplerPub` instance. Args: - pub: an object to be sampler pub. + pub: An object to coerce. + shots: An optional default number of shots to use if not + already specified by the pub-like object. Returns: - A coerced estiamtor pub. - - Raises: - ValueError: If input values are invalid. + A coerced sampler pub. """ + # Validate shots kwarg if provided + if shots is not None: + if not isinstance(shots, Integral) or isinstance(shots, bool): + raise TypeError("shots must be an integer") + if shots < 0: + raise ValueError("shots must be non-negative") + if isinstance(pub, SamplerPub): + if pub.shots is None and shots is not None: + return cls( + circuit=pub.circuit, + parameter_values=pub.parameter_values, + shots=shots, + validate=False, # Assume Pub is already validated + ) return pub - if len(pub) != 1 and len(pub) != 2: - raise ValueError(f"The length of pub must be 1 or 2, but length {len(pub)} is given.") + + if isinstance(pub, QuantumCircuit): + return cls(circuit=pub, shots=shots, validate=True) + + if len(pub) not in [1, 2, 3]: + raise ValueError( + f"The length of pub must be 1, 2 or 3, but length {len(pub)} is given." + ) circuit = pub[0] - parameter_values = ( - BindingsArray.coerce(pub[1]) if len(pub) == 2 else BindingsArray([], shape=()) - ) - return cls(circuit=circuit, parameter_values=parameter_values) + parameter_values = BindingsArray.coerce(pub[1]) if len(pub) > 1 else None + if len(pub) > 2 and pub[2] is not None: + shots = pub[2] + return cls(circuit=circuit, parameter_values=parameter_values, shots=shots, validate=True) - def validate(self) -> None: - """Validate the pub. + def validate(self): + """Validate the pub.""" + if not isinstance(self.circuit, QuantumCircuit): + raise TypeError("circuit must be QuantumCircuit.") - Raises: - ValueError: If input values are invalid. - """ - super().validate() self.parameter_values.validate() - # Cross validate circuits and paramter_values + + if self.shots is not None: + if not isinstance(self.shots, Integral) or isinstance(self.shots, bool): + raise TypeError("shots must be an integer") + if self.shots < 0: + raise ValueError("shots must be non-negative") + + # Cross validate circuits and parameter values num_parameters = self.parameter_values.num_parameters if num_parameters != self.circuit.num_parameters: raise ValueError( @@ -79,4 +141,10 @@ def validate(self) -> None: ) -SamplerPubLike = Union[SamplerPub, Tuple[QuantumCircuit, BindingsArrayLike]] +SamplerPubLike = Union[ + SamplerPub, + QuantumCircuit, + Tuple[QuantumCircuit], + Tuple[QuantumCircuit, BindingsArrayLike], + Tuple[QuantumCircuit, BindingsArrayLike, Union[Integral, None]], +] From cf82488e3d48021858d75751512bc2d6e2178f96 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Thu, 18 Jan 2024 14:35:13 -0500 Subject: [PATCH 02/19] update encoder & fix linting --- qiskit_ibm_runtime/estimator.py | 17 +- .../primitives/base/base_primitive_job.py | 78 ++++++ .../qiskit/primitives/base/base_result.py | 87 +++++++ .../qiskit/primitives/base_estimator.py | 6 + .../qiskit/primitives/base_pub.py | 47 ---- .../qiskit/primitives/bindings_array.py | 228 ++++++++---------- .../qiskit/primitives/containers/data_bin.py | 82 +++++++ .../primitives/containers/primitive_result.py | 52 ++++ .../primitives/containers/pub_result.py | 45 ++++ qiskit_ibm_runtime/utils/json.py | 13 +- 10 files changed, 475 insertions(+), 180 deletions(-) create mode 100644 qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py create mode 100644 qiskit_ibm_runtime/qiskit/primitives/base/base_result.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base_pub.py create mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py create mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py create mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 356d85914..4efa045cf 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -135,18 +135,27 @@ def __init__( def run( self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike], precision: float | None = None ) -> RuntimeJob: - """TODO: docstring""" + """Submit a request to the estimator primitive. + + Args: + pubs: A pub-like (primitive unified bloc) object, such as a tuple, + ``(circuit, observables, parameter_values)``, or a list of pub-like objects. + precision: Target precision for expectation value estimates.. + + Returns: + Submitted job. + + """ if isinstance(pubs, EstimatorPub): pubs = [pubs] elif isinstance(pubs, tuple) and isinstance(pubs[0], QuantumCircuit): - pubs = [EstimatorPub.coerce(pubs)] + pubs = [EstimatorPub.coerce(pubs, precision=precision)] elif pubs is not EstimatorPub: - pubs = [EstimatorPub.coerce(pub) for pub in pubs] + pubs = [EstimatorPub.coerce(pub, precision=precision) for pub in pubs] for pub in pubs: pub.validate() - print(pubs[0], type(pubs[0])) return self._run(pubs) def _validate_options(self, options: dict) -> None: diff --git a/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py b/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py new file mode 100644 index 000000000..9dca27c0c --- /dev/null +++ b/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py @@ -0,0 +1,78 @@ +# 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. +""" +Primitive job abstract base class +""" + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Union + +from ..containers.primitive_result import PrimitiveResult +from .base_result import BasePrimitiveResult + +ResultT = TypeVar("ResultT", bound=Union[BasePrimitiveResult, PrimitiveResult]) +StatusT = TypeVar("StatusT") + + +class BasePrimitiveJob(ABC, Generic[ResultT, StatusT]): + """Primitive job abstract base class.""" + + def __init__(self, job_id: str, **kwargs) -> None: + """Initializes the primitive job. + + Args: + job_id: A unique id in the context of the primitive used to run the job. + kwargs: Any key value metadata to associate with this job. + """ + self._job_id = job_id + self.metadata = kwargs + + def job_id(self) -> str: + """Return a unique id identifying the job.""" + return self._job_id + + @abstractmethod + def result(self) -> ResultT: + """Return the results of the job.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `result` method.") + + @abstractmethod + def status(self) -> StatusT: + """Return the status of the job.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `status` method.") + + @abstractmethod + def done(self) -> bool: + """Return whether the job has successfully run.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `done` method.") + + @abstractmethod + def running(self) -> bool: + """Return whether the job is actively running.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `running` method.") + + @abstractmethod + def cancelled(self) -> bool: + """Return whether the job has been cancelled.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `cancelled` method.") + + @abstractmethod + def in_final_state(self) -> bool: + """Return whether the job is in a final job state such as ``DONE`` or ``ERROR``.""" + raise NotImplementedError( + "Subclass of BasePrimitiveJob must implement `is_final_state` method." + ) + + @abstractmethod + def cancel(self): + """Attempt to cancel the job.""" + raise NotImplementedError("Subclass of BasePrimitiveJob must implement `cancel` method.") diff --git a/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py b/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py new file mode 100644 index 000000000..2c23ea1d7 --- /dev/null +++ b/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Primitive result abstract base class +""" + +from __future__ import annotations + +from abc import ABC +from collections.abc import Iterator, Sequence +from dataclasses import fields +from typing import Any, Dict + +from numpy import ndarray + + +ExperimentData = Dict[str, Any] + + +class BasePrimitiveResult(ABC): + """Primitive result abstract base class. + + Base class for Primitive results meant to provide common functionality to all inheriting + result dataclasses. + """ + + def __post_init__(self) -> None: + """ + Verify that all fields in any inheriting result dataclass are consistent, after + instantiation, with the number of experiments being represented. + + This magic method is specific of `dataclasses.dataclass`, therefore all inheriting + classes must have this decorator. + + Raises: + TypeError: If one of the data fields is not a Sequence or ``numpy.ndarray``. + ValueError: Inconsistent number of experiments across data fields. + """ + for value in self._field_values: # type: Sequence + # TODO: enforce all data fields to be tuples instead of sequences + if not isinstance(value, (Sequence, ndarray)) or isinstance(value, (str, bytes)): + raise TypeError( + f"Expected sequence or `numpy.ndarray`, provided {type(value)} instead." + ) + if len(value) != self.num_experiments: + raise ValueError("Inconsistent number of experiments across data fields.") + + @property # TODO: functools.cached_property when py37 is droppped + def num_experiments(self) -> int: + """Number of experiments in any inheriting result dataclass.""" + value: Sequence = self._field_values[0] + return len(value) + + @property # TODO: functools.cached_property when py37 is droppped + def experiments(self) -> tuple[ExperimentData, ...]: + """Experiment data dicts in any inheriting result dataclass.""" + return tuple(self._generate_experiments()) + + def _generate_experiments(self) -> Iterator[ExperimentData]: + """Generate experiment data dicts in any inheriting result dataclass.""" + names: tuple[str, ...] = self._field_names + for values in zip(*self._field_values): + yield dict(zip(names, values)) + + def decompose(self) -> Iterator[BasePrimitiveResult]: + """Generate single experiment result objects from self.""" + for values in zip(*self._field_values): + yield self.__class__(*[(v,) for v in values]) + + @property # TODO: functools.cached_property when py37 is droppped + def _field_names(self) -> tuple[str, ...]: + """Tuple of field names in any inheriting result dataclass.""" + return tuple(field.name for field in fields(self)) + + @property # TODO: functools.cached_property when py37 is droppped + def _field_values(self) -> tuple: + """Tuple of field values in any inheriting result dataclass.""" + return tuple(getattr(self, name) for name in self._field_names) diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py index 2ddc9bf24..33ff44a75 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py +++ b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py @@ -88,6 +88,12 @@ from .estimator_pub import EstimatorPub, EstimatorPubLike +# TODO remove this after released in qiskit +from .containers.data_bin import DataBin, make_data_bin +from .containers.primitive_result import PrimitiveResult +from .containers.pub_result import PubResult +from .base.base_primitive_job import BasePrimitiveJob + T = TypeVar("T", bound=Job) # pylint: disable=invalid-name diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_pub.py b/qiskit_ibm_runtime/qiskit/primitives/base_pub.py deleted file mode 100644 index 238463ade..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_pub.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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. - -""" -Base Pubs class -""" - -from __future__ import annotations - -from qiskit import QuantumCircuit - - -class BasePub: - """Base class for PUB (Primitive Unified Bloc)""" - - __slots__ = ("_circuit",) - - def __init__(self, circuit: QuantumCircuit, validate: bool = False): - """ - Initialize a BasePub. - - Args: - circuit: Quantum circuit object for the pubs. - validate: if True, the input data is validated during initialization. - """ - self._circuit = circuit - if validate: - self.validate() - - @property - def circuit(self) -> QuantumCircuit: - """A quantum circuit for the pub""" - return self._circuit - - def validate(self): - """Validate the data""" - if not isinstance(self.circuit, QuantumCircuit): - raise TypeError("circuit must be QuantumCircuit.") diff --git a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py index 4aa5096b8..bf3efd37d 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py +++ b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.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 @@ -9,47 +9,46 @@ # 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. -# type: ignore """ Bindings array class """ from __future__ import annotations -from collections.abc import Iterable -from itertools import chain, product -from typing import Dict, List, Optional, Tuple, Union, Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence +from itertools import chain, islice +from typing import Union import numpy as np -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parameterexpression import ParameterValueType from .shape import ShapedMixin, ShapeInput, shape_tuple +ParameterLike = Union[Parameter, str] + class BindingsArray(ShapedMixin): - r"""Stores many possible parameter binding values for a :class:`qiskit.QuantumCircuit`. + r"""Stores parameter binding value sets for a :class:`qiskit.QuantumCircuit`. - Similar to a ``inspect.BoundArguments`` instance, which stores arguments that can be bound to a - compatible Python function, this class stores both values without names, so that their ordering - is important, as well as values attached to ``qiskit.circuit.Parameters``. However, a dense - rectangular array of possible values is stored for each parameter, so that this class is akin to - an object-array of ``inspect.BoundArguments``. + A single parameter binding set provides numeric values to bind to a circuit with free + :class:`qiskit.circuit.Parameter`\s. An instance of this class stores an array-valued + collection of such sets. The simplest example is a 0-d array consisting of a single + parameter binding set, whereas an n-d array of parameter binding sets represents an + n-d sweep over values. The storage format is a list of arrays, ``[vals0, vals1, ...]``, as well as a dictionary of - arrays attached to parameters, ``{params0: kwvals0, ...}``. Crucially, the last dimension of - each array indexes one or more parameters. For example, if the last dimension of ``vals1`` is - 25, then it represents an array of possible binding values for 25 distinct parameters, where its - leading shape is the array :attr:`~.shape` of its binding array. This implies a degeneracy of the - storage format: ``[vals, vals1[..., :10], vals1[..., 10:], ...]`` is exactly equivalent to - ``[vals0, vals1, ...]`` in the bindings it specifies. This complication has been included to - satisfy two competing constraints: - - * Arrays with different dtypes cannot be concatenated into a single array, so that multiple - arrays are required for generality. - * It is extremely convenient to put everything into a small number of big arrays, when - possible. + arrays attached to parameters, ``{params0: kwvals0, ...}``. A convention is used + where the last dimension of each array indexes (a subset of) circuit parameters. For + example, if the last dimension of ``vals1`` is 25, then it represents an array of + possible binding values for 25 distinct parameters, where its leading shape is the + array :attr:`~.shape` of its binding array. This implies a degeneracy of the storage + format: ``[vals, vals1[..., :10], vals1[..., 10:], ...]`` is exactly equivalent to + ``[vals0, vals1, ...]`` in the bindings it specifies. This allows flexibility about whether + values for different parameters are stored in one big array, or across several smaller + arrays. It also allows different parameters to use different dtypes. .. code-block:: python @@ -66,14 +65,17 @@ class BindingsArray(ShapedMixin): {("c", "a"): np.empty((10, 10, 2)), "b": np.empty((10, 10))} ) """ + __slots__ = ("_vals", "_kwvals") def __init__( self, - vals: Union[None, ArrayLike, Iterable[ArrayLike]] = None, - kwvals: Union[None, Mapping[Parameter, Iterable[Parameter]], ArrayLike] = None, - shape: Optional[ShapeInput] = None, + vals: ArrayLike | Iterable[ArrayLike] | None = None, + kwvals: Mapping[ParameterLike, Iterable[ParameterValueType]] | ArrayLike | None = None, + shape: ShapeInput | None = None, ): - """ + r""" + Initialize a ``BindingsArray``. It can take parameter vectors and dictionaries. + The ``shape`` argument does not need to be provided whenever it can unambiguously be inferred from the provided arrays. Ambiguity arises because an array provided to the constructor might represent values for either a single parameter, with an implicit missing @@ -85,7 +87,7 @@ def __init__( it is assumed that the last dimension is over many parameters. * Multiple arrays are given whose shapes differ only in the last dimension size. * Some array is given in ``kwvals`` where the key contains multiple - :class:`~.Parameter` s, whose length the last dimension of the array must therefore match. + :class:`~.Parameter`\s, whose length the last dimension of the array must therefore match. Args: vals: One or more arrays, where the last index of each corresponds to @@ -109,7 +111,9 @@ def __init__( vals = [vals] if isinstance(vals, np.ndarray) else [np.array(v, copy=False) for v in vals] kwvals = { - (p,) if isinstance(p, Parameter) else tuple(p): np.array(val, copy=False) + _format_key((p,)) + if isinstance(p, Parameter) + else _format_key(p): np.array(val, copy=False) for p, val in kwvals.items() } @@ -121,16 +125,12 @@ def __init__( self._shape = shape_tuple(shape) for idx, val in enumerate(vals): vals[idx] = _standardize_shape(val, self._shape) - for parameters, val in kwvals.items(): - val = kwvals[parameters] = _standardize_shape(val, self._shape) - if len(parameters) != val.shape[-1]: - raise ValueError( - f"Length of {parameters} inconsistent with last dimension of {val}" - ) - self._vals = vals + self._vals: list[np.ndarray] = vals self._kwvals = kwvals + self.validate() + def __getitem__(self, args) -> BindingsArray: # because the parameters live on the last axis, we don't need to do anything special to # accomodate them because there will always be an implicit slice(None, None, None) @@ -145,22 +145,31 @@ def __getitem__(self, args) -> BindingsArray: shape = () return BindingsArray(vals, kwvals, shape) + def __repr__(self): + descriptions = [f"shape={self.shape}", f"num_parameters={self.num_parameters}"] + if num_kwval_params := sum(val.shape[-1] for val in self._kwvals.values()): + names = list(islice(map(repr, chain.from_iterable(map(_format_key, self._kwvals))), 5)) + if len(names) < num_kwval_params: + names.append("...") + descriptions.append(f"parameters=[{', '.join(names)}]") + return f"{type(self).__name__}(<{', '.join(descriptions)}>)" + @property - def kwvals(self) -> Dict[Tuple[Parameter, ...], np.ndarray]: + def kwvals(self) -> dict[tuple[str, ...], np.ndarray]: """The keyword values of this array.""" return self._kwvals @property def num_parameters(self) -> int: """The total number of parameters.""" - return sum(val.shape[-1] for val in chain(self.vals, self.kwvals.values())) + return sum(val.shape[-1] for val in chain(self.vals, self._kwvals.values())) @property - def vals(self) -> List[np.ndarray]: + def vals(self) -> list[np.ndarray]: """The non-keyword values of this array.""" return self._vals - def as_array(self, parameters: Optional[Iterable[Parameter]] = None) -> np.ndarray: + def as_array(self, parameters: Iterable[Parameter | str] | None = None) -> np.ndarray: """Return the contents of this bindings array as a single NumPy array. As with each :attr:`~vals` and :attr:`~kwvals` array, the parameters are indexed along the @@ -172,7 +181,7 @@ def as_array(self, parameters: Optional[Iterable[Parameter]] = None) -> np.ndarr If ``parameters`` are provided, then they determine the order of any :attr:`~kwvals` present in this bindings array. If :attr:`~vals` are present in addition to :attr:`~kwvals`, - then it is up to the user to ensure that their provided ``parameters`` account for this. + then they appear before the :attr:`~kwvals` always. Parameters: parameters: Optional parameters that determine the order of the output. @@ -181,9 +190,8 @@ def as_array(self, parameters: Optional[Iterable[Parameter]] = None) -> np.ndarr This bindings array as a single NumPy array. Raises: - RuntimeError: If these bindings contain multple dtypes. - KeyError: If ``parameters`` are provided that are not a superset of those in this - bindings array. + RuntimeError: If these bindings contain multiple dtypes. + ValueError: If ``parameters`` are provided, but do not match those found in ``kwvals``. """ dtypes = {arr.dtype for arr in self.vals} dtypes.update(arr.dtype for arr in self.kwvals.values()) @@ -204,12 +212,6 @@ def as_array(self, parameters: Optional[Iterable[Parameter]] = None) -> np.ndarr ret[..., pos : pos + size] = arr pos += size - def _param_name(parameter: Union[Parameter, str]) -> str: - """Helper function to handle parameters or strings""" - if isinstance(parameter, Parameter): - return parameter.name - return parameter - if parameters is None: # preserve the order of the kwvals for arr in self.kwvals.values(): @@ -218,24 +220,27 @@ def _param_name(parameter: Union[Parameter, str]) -> str: pos += size elif self.kwvals: # use the order of the provided parameters - parameters = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)} + parameters = list(parameters) + if len(parameters) != (num_kwval := sum(arr.shape[-1] for arr in self.kwvals.values())): + raise ValueError(f"Expected {num_kwval} parameters but {len(parameters)} received.") + + idx_lookup = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)} for arr_params, arr in self.kwvals.items(): try: - idxs = [parameters[_param_name(param)] for param in arr_params] + idxs = [idx_lookup[_param_name(param)] + pos for param in arr_params] except KeyError as ex: - raise KeyError( - "This bindings array has a parameter absent from the provided parameters." - ) from ex + missing = next(p for p in map(_param_name, arr_params) if p not in idx_lookup) + raise ValueError(f"Could not find placement for parameter '{missing}'.") from ex ret[..., idxs] = arr return ret - def bind_at_idx(self, circuit: QuantumCircuit, idx: Tuple[int, ...]) -> QuantumCircuit: - """Return the circuit bound to the values at the provided index. + def bind(self, circuit: QuantumCircuit, loc: tuple[int, ...]) -> QuantumCircuit: + """Return a new circuit bound to the values at the provided index. Args: circuit: The circuit to bind. - idx: A tuple of indices, on for each dimension of this array. + loc: A tuple of indices, on for each dimension of this array. Returns: The bound circuit. @@ -243,35 +248,23 @@ def bind_at_idx(self, circuit: QuantumCircuit, idx: Tuple[int, ...]) -> QuantumC Raises: ValueError: If the index doesn't have the right number of values. """ - if len(idx) != self.ndim: - raise ValueError(f"Expected {idx} to index all dimensions of {self.shape}") + if len(loc) != self.ndim: + raise ValueError(f"Expected {loc} to index all dimensions of {self.shape}") - flat_vals = (val for vals in self.vals for val in vals[idx]) + flat_vals = (val for vals in self.vals for val in vals[loc]) - if not self.kwvals: + if not self._kwvals: # special case to avoid constructing a dictionary input return circuit.assign_parameters(list(flat_vals)) parameters = dict(zip(circuit.parameters, flat_vals)) parameters.update( (param, val) - for params, vals in self.kwvals.items() - for param, val in zip(params, vals[idx]) + for params, vals in self._kwvals.items() + for param, val in zip(params, vals[loc]) ) return circuit.assign_parameters(parameters) - def bind_flat(self, circuit: QuantumCircuit) -> Iterable[QuantumCircuit]: - """Yield a bound circuit for every array index in flattened order. - - Args: - circuit: The circuit to bind. - - Yields: - Bound circuits, in flattened array order. - """ - for idx in product(*map(range, self.shape)): - yield self.bind_at_idx(circuit, idx) - def bind_all(self, circuit: QuantumCircuit) -> np.ndarray: """Return an object array of bound circuits with the same shape. @@ -283,7 +276,7 @@ def bind_all(self, circuit: QuantumCircuit) -> np.ndarray: """ arr = np.empty(self.shape, dtype=object) for idx in np.ndindex(self.shape): - arr[idx] = self.bind_at_idx(circuit, idx) + arr[idx] = self.bind(circuit, idx) return arr def ravel(self) -> BindingsArray: @@ -297,13 +290,13 @@ def ravel(self) -> BindingsArray: """ return self.reshape(self.size) - def reshape(self, *shape: ShapeInput) -> BindingsArray: + def reshape(self, shape: int | Iterable[int]) -> BindingsArray: """Return a new :class:`~BindingsArray` with a different shape. This results in a new view of the same arrays. Args: - *shape: The shape of the returned bindings array. + shape: The shape of the returned bindings array. Returns: A new bindings array. @@ -311,69 +304,46 @@ def reshape(self, *shape: ShapeInput) -> BindingsArray: Raises: ValueError: If the provided shape has a different product than the current size. """ - shape = shape_tuple(shape) - - # if we have a minus 1, try and replace it with with a positive number - if any(dim < 0 for dim in shape): - if (subsize := np.prod([dim for dim in shape if dim >= 0]).astype(int)) > 0: - shape = tuple(dim if dim > 0 else self.size // subsize for dim in shape) - - if np.prod(shape).astype(int) != self.size: - raise ValueError(f"Reshaping cannot change the total number of elements. {shape}") - - vals = [val.reshape(shape + (val.shape[-1],)) for val in self._vals] - kwvals = { - params: val.reshape(shape + (val.shape[-1],)) for params, val in self._kwvals.items() - } - return BindingsArray(vals, kwvals, shape) + shape = (shape, -1) if isinstance(shape, int) else (*shape, -1) + if np.prod(shape[:-1]).astype(int) != self.size: + raise ValueError("Reshaping cannot change the total number of elements.") + vals = [val.reshape(shape) for val in self._vals] + kwvals = {params: val.reshape(shape) for params, val in self._kwvals.items()} + return BindingsArray(vals, kwvals, shape[:-1]) @classmethod def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray: - """Coerce BindingsArrayLike into BindingsArray + """Coerce an input that is :class:`~BindingsArrayLike` into a new :class:`~BindingsArray`. Args: - bindings_array: an object to be bindings array. + bindings_array: An object to be bindings array. Returns: - A coerced bindings array. - - Raises: - TypeError: If input value type is invalid. + A new bindings array. """ - if isinstance(bindings_array, BindingsArray): - return bindings_array if isinstance(bindings_array, Sequence): bindings_array = np.array(bindings_array) if bindings_array is None: - bindings_array = cls([], shape=(1,)) + bindings_array = cls() elif isinstance(bindings_array, np.ndarray): - if bindings_array.ndim == 1: - bindings_array = bindings_array.reshape((1, -1)) bindings_array = cls(bindings_array) elif isinstance(bindings_array, Mapping): bindings_array = cls(kwvals=bindings_array) else: - raise TypeError( - f"Parameter values type {type(bindings_array)} is not BindingsArray-like." - ) + raise TypeError(f"Unsupported type {type(bindings_array)} is given.") return bindings_array def validate(self): """Validate the consistency in bindings_array.""" - for val in self.vals: - if not isinstance(val, np.ndarray) and val.dtype != float: - raise TypeError( - f"Invalid individual parameter value type {type(val)}, should be a float." - ) - for par, val in self.kwvals.items(): - if not isinstance(val, np.ndarray) and val.dtype != float: - raise TypeError( - f"Invalid individual parameter value type {type(val)} " - f"for parameter {par}, should be a float." + for parameters, val in self._kwvals.items(): + val = self._kwvals[parameters] = _standardize_shape(val, self._shape) + if len(parameters) != val.shape[-1]: + raise ValueError( + f"Length of {parameters} inconsistent with last dimension of {val}" ) -def _standardize_shape(val: np.ndarray, shape: Tuple[int, ...]) -> np.ndarray: +def _standardize_shape(val: np.ndarray, shape: tuple[int, ...]) -> np.ndarray: """Return ``val`` or ``val[..., None]``. Args: @@ -395,8 +365,8 @@ def _standardize_shape(val: np.ndarray, shape: Tuple[int, ...]) -> np.ndarray: def _infer_shape( - vals: List[np.ndarray], kwvals: Dict[Tuple[Parameter, ...], np.ndarray] -) -> Tuple[int, ...]: + vals: list[np.ndarray], kwvals: dict[tuple[Parameter, ...], np.ndarray] +) -> tuple[int, ...]: """Return a shape tuple that consistently defines the leading dimensions of all arrays. Args: @@ -446,10 +416,18 @@ def examine_array(*possible_shapes): raise ValueError("Could not unambiguously determine the intended shape; specify shape manually") +def _format_key(key: tuple[Parameter | str, ...]): + return tuple(map(_param_name, key)) + + +def _param_name(param: Parameter | str) -> str: + return param.name if isinstance(param, Parameter) else param + + BindingsArrayLike = Union[ BindingsArray, - NDArray, - Mapping[Parameter, NDArray], - Sequence[NDArray], + ArrayLike, + "Mapping[Parameter, ArrayLike]", + "Sequence[ArrayLike]", None, ] diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py b/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py new file mode 100644 index 000000000..aab2b083d --- /dev/null +++ b/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py @@ -0,0 +1,82 @@ +# 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. + +""" +Dataclass tools for data namespaces (bins) +""" +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import make_dataclass + + +class DataBinMeta(type): + """Metaclass for :class:`DataBin` that adds the shape to the type name. + This is so that the class has a custom repr with DataBin<*shape> notation. + """ + + def __repr__(cls): + name = cls.__name__ + if cls._SHAPE is None: + return name + shape = ",".join(map(str, cls._SHAPE)) + return f"{name}<{shape}>" + + +class DataBin(metaclass=DataBinMeta): + """Base class for data bin containers. + Subclasses are typically made via :class:`~make_data_bin`, which is a specialization of + :class:`make_dataclass`. + """ + + _RESTRICTED_NAMES = ("_RESTRICTED_NAMES", "_SHAPE", "_FIELDS", "_FIELD_TYPES") + _SHAPE: tuple[int, ...] | None = None + _FIELDS: tuple[str, ...] = () + """The fields allowed in this data bin.""" + _FIELD_TYPES: tuple[type, ...] = () + """The types of each field.""" + + def __repr__(self): + vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) + return f"{type(self)}({', '.join(vals)})" + + +def make_data_bin( + fields: Iterable[tuple[str, type]], shape: tuple[int, ...] | None = None +) -> DataBinMeta: + """Return a new subclass of :class:`~DataBin` with the provided fields and shape. + .. code-block:: python + my_bin = make_data_bin([("alpha", np.NDArray[np.float])], shape=(20, 30)) + # behaves like a dataclass + my_bin(alpha=np.empty((20, 30))) + Args: + fields: Tuples ``(name, type)`` specifying the attributes of the returned class. + shape: The intended shape of every attribute of this class. + Returns: + A new class. + """ + field_names, field_types = zip(*fields) + for name in field_names: + if name in DataBin._RESTRICTED_NAMES: + raise ValueError(f"'{name}' is a restricted name for a DataBin.") + cls = make_dataclass( + "DataBin", + dict(zip(field_names, field_types)), + bases=(DataBin,), + frozen=True, + unsafe_hash=True, + repr=False, + ) + cls._SHAPE = shape + cls._FIELDS = field_names + cls._FIELD_TYPES = field_types + return cls diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py b/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py new file mode 100644 index 000000000..b383f4e11 --- /dev/null +++ b/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py @@ -0,0 +1,52 @@ +# 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. + +"""PrimitiveResult""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any, Generic, TypeVar + +from .pub_result import PubResult + +T = TypeVar("T", bound=PubResult) + + +class PrimitiveResult(Generic[T]): + """A container for multiple pub results and global metadata.""" + + def __init__(self, pub_results: Iterable[T], metadata: dict[str, Any] | None = None): + """ + Args: + pub_results: Pub results. + metadata: Any metadata that doesn't make sense to put inside of pub results. + """ + self._pub_results = list(pub_results) + self._metadata = metadata or {} + + @property + def metadata(self) -> dict[str, Any]: + """The metadata of this primitive result.""" + return self._metadata + + def __getitem__(self, index) -> T: + return self._pub_results[index] + + def __len__(self) -> int: + return len(self._pub_results) + + def __repr__(self) -> str: + return f"PrimitiveResult({self._pub_results}, metadata={self.metadata})" + + def __iter__(self) -> Iterable[T]: + return iter(self._pub_results) diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py b/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py new file mode 100644 index 000000000..c7ac2b616 --- /dev/null +++ b/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py @@ -0,0 +1,45 @@ +# 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. + +""" +Base Pub class +""" + +from __future__ import annotations + +from .data_bin import DataBin + + +class PubResult: + """Result of Primitive Unified Bloc.""" + + __slots__ = ("_data", "_metadata") + + def __init__(self, data: DataBin, metadata: dict | None = None): + """Initialize a pub result. + + Args: + data: result data bin. + metadata: metadata dictionary. + """ + self._data = data + self._metadata = metadata or {} + + @property + def data(self) -> DataBin: + """Result data for the pub.""" + return self._data + + @property + def metadata(self) -> dict: + """Metadata for the pub.""" + return self._metadata diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index fcc634c9e..f6ba10c00 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -26,7 +26,6 @@ import zlib from datetime import date from typing import Any, Callable, Dict, List, Union, Tuple -from dataclasses import asdict import dateutil.parser import numpy as np @@ -66,10 +65,10 @@ dump, load, ) +from qiskit_ibm_runtime.qiskit.primitives.estimator_pub import EstimatorPub # TODO: Remove when they are in terra from ..qiskit.primitives import ObservablesArray, BindingsArray -from ..qiskit.primitives.base_pub import BasePub _TERRA_VERSION = tuple( int(x) for x in re.match(r"\d+\.\d+\.\d", _terra_version_string).group(0).split(".")[:3] @@ -254,8 +253,14 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ serializer=lambda buff, data: dump(data, buff), # type: ignore[no-untyped-call] ) return {"__type__": "Instruction", "__value__": value} - if isinstance(obj, BasePub): - return asdict(obj) + # TODO proper way to do this? + if isinstance(obj, EstimatorPub): + return { + "circuit": obj.circuit, + "observables": obj.observables, + "parameter_values": obj.parameter_values, + "precision": obj.precision, + } if isinstance(obj, ObservablesArray): return obj.tolist() if isinstance(obj, BindingsArray): From bb8125ba6f18d9d25010268034b62da0640399ab Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Thu, 18 Jan 2024 15:08:54 -0500 Subject: [PATCH 03/19] fix mypy & lint --- qiskit_ibm_runtime/base_primitive.py | 2 +- qiskit_ibm_runtime/estimator.py | 2 +- .../qiskit/primitives/bindings_array.py | 2 ++ qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py | 3 ++- .../qiskit/primitives/observables_array.py | 2 ++ qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py | 11 ++++++----- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 48e6ded9f..139c28d2f 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -181,7 +181,7 @@ def _set_options(self, options: Optional[Union[Dict, BaseOptions]] = None) -> No self._options = self._options_class() elif isinstance(options, dict): default_options = self._options_class() - self.options = self._options_class(**merge_options(default_options, options)) + self._options = self._options_class(**merge_options(default_options, options)) elif isinstance(options, self._options_class): self._options = replace(options) else: diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 4efa045cf..ce497ab96 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -154,7 +154,7 @@ def run( pubs = [EstimatorPub.coerce(pub, precision=precision) for pub in pubs] for pub in pubs: - pub.validate() + pub.validate() # type: ignore[union-attr] return self._run(pubs) diff --git a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py index bf3efd37d..b22211b2d 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py +++ b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# Copied from qiskit, should be removed after first release +# mypy: ignore-errors """ Bindings array class """ diff --git a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py b/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py index 992c78bba..d273e9990 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py +++ b/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py @@ -10,7 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - +# Copied from qiskit, should be removed after first release +# mypy: ignore-errors """ Estimator Pub class """ diff --git a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py b/qiskit_ibm_runtime/qiskit/primitives/observables_array.py index 12dd51837..981b6ef5f 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py +++ b/qiskit_ibm_runtime/qiskit/primitives/observables_array.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# Copied from qiskit, should be removed after first release +# mypy: ignore-errors """ ND-Array container class for Estimator observables. diff --git a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py b/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py index 76d5a68e6..6b90a83ed 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py +++ b/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py @@ -10,7 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - +# Copied from qiskit, should be removed after first release +# mypy: ignore-errors """ Sampler Pub class """ @@ -93,8 +94,8 @@ def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: if shots is not None: if not isinstance(shots, Integral) or isinstance(shots, bool): raise TypeError("shots must be an integer") - if shots < 0: - raise ValueError("shots must be non-negative") + if shots <= 0: + raise ValueError("shots must be positive") if isinstance(pub, SamplerPub): if pub.shots is None and shots is not None: @@ -129,8 +130,8 @@ def validate(self): if self.shots is not None: if not isinstance(self.shots, Integral) or isinstance(self.shots, bool): raise TypeError("shots must be an integer") - if self.shots < 0: - raise ValueError("shots must be non-negative") + if self.shots <= 0: + raise ValueError("shots must be positive") # Cross validate circuits and parameter values num_parameters = self.parameter_values.num_parameters From 3f28c19f23bc131bd8bcfc84268ff0bd3e4aadea Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Thu, 18 Jan 2024 17:16:41 -0500 Subject: [PATCH 04/19] update unit tests --- qiskit_ibm_runtime/base_primitive.py | 2 +- qiskit_ibm_runtime/qiskit/primitives/bindings_array.py | 4 ++++ test/unit/test_data_serialization.py | 3 ++- test/unit/test_estimator.py | 4 ++-- test/unit/test_ibm_primitives_v2.py | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 139c28d2f..77312a69a 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -127,7 +127,7 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJob: Returns: Submitted job. """ - primitive_inputs = {"tasks": pubs} + primitive_inputs = {"pubs": pubs} options_dict = asdict(self.options) self._validate_options(options_dict) primitive_inputs.update(self._options_class._get_program_inputs(options_dict)) diff --git a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py index b22211b2d..6f84ca46f 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py +++ b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py @@ -323,6 +323,10 @@ def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray: Returns: A new bindings array. """ + # TODO added this - if the value passed in is already a BindingsArray, + # it should be returned instead of throwing a type error right? + if isinstance(bindings_array, BindingsArray): + return bindings_array if isinstance(bindings_array, Sequence): bindings_array = np.array(bindings_array) if bindings_array is None: diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index 2b3a63e42..758f62520 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -309,7 +309,8 @@ def test_bindings_array(self, barray): def _to_str_keyed(_in_dict): _out_dict = {} for a_key_tuple, val in _in_dict.items(): - str_key = tuple(a_key.name for a_key in a_key_tuple) + # TODO double check this is correct + str_key = tuple(a_key for a_key in a_key_tuple) _out_dict[str_key] = val return _out_dict diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index a0b497c63..85f7d47bf 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -77,8 +77,8 @@ def test_run_program_inputs(self, in_pubs): inst = EstimatorV2(session=session) inst.run(in_pubs) input_params = session.run.call_args.kwargs["inputs"] - self.assertIn("tasks", input_params) - pubs_param = input_params["tasks"] + self.assertIn("pubs", input_params) + pubs_param = input_params["pubs"] for a_pub_param, an_in_taks in zip(pubs_param, in_pubs): self.assertIsInstance(a_pub_param, EstimatorPub) # Check circuit diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index 0af472596..44843d3ab 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -427,7 +427,7 @@ def test_run_unset_options(self, primitive): inst = primitive(session=session) inst.run(**get_primitive_inputs(inst)) inputs = session.run.call_args.kwargs["inputs"] - for fld in ["tasks", "_experimental"]: + for fld in ["pubs", "_experimental"]: inputs.pop(fld, None) expected = {"skip_transpilation": False, "execution": {"init_qubits": True}, "version": 2} self.assertDictEqual(inputs, expected) From b5f5b36d0bf66629c6b933654fc52cbb0d6fd9ab Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Sun, 21 Jan 2024 23:59:17 -0500 Subject: [PATCH 05/19] pubsresult -> pubresult --- qiskit_ibm_runtime/qiskit/primitives/pub_result.py | 3 ++- qiskit_ibm_runtime/utils/estimator_result_decoder.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit/primitives/pub_result.py b/qiskit_ibm_runtime/qiskit/primitives/pub_result.py index 467bfb9b3..76145cbdc 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/pub_result.py +++ b/qiskit_ibm_runtime/qiskit/primitives/pub_result.py @@ -9,6 +9,7 @@ # 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. +# TODO replace with file in qiskit/primitives/containers/pub_result.py """ Base Pub class @@ -20,7 +21,7 @@ @dataclass(frozen=True) -class PubsResult: +class PubResult: """Result of pub.""" data: dict diff --git a/qiskit_ibm_runtime/utils/estimator_result_decoder.py b/qiskit_ibm_runtime/utils/estimator_result_decoder.py index 3d3583681..904745530 100644 --- a/qiskit_ibm_runtime/utils/estimator_result_decoder.py +++ b/qiskit_ibm_runtime/utils/estimator_result_decoder.py @@ -18,7 +18,7 @@ from qiskit.primitives import EstimatorResult from .result_decoder import ResultDecoder -from ..qiskit.primitives.pub_result import PubsResult +from ..qiskit.primitives.pub_result import PubResult class EstimatorResultDecoder(ResultDecoder): @@ -36,7 +36,7 @@ def decode( # type: ignore # pylint: disable=arguments-differ if not isinstance(val, np.ndarray): val = np.asarray(val) out_results.append( - PubsResult(data={"evs": val, "stds": meta.pop("standard_error")}, metadata=meta) + PubResult(data={"evs": val, "stds": meta.pop("standard_error")}, metadata=meta) ) return out_results return EstimatorResult( From 2b1334a5690da49e2c4290f2d1a9de62ed14acf7 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 22 Jan 2024 11:39:00 -0500 Subject: [PATCH 06/19] Update estimator result --- .../qiskit/primitives/containers/data_bin.py | 12 ++++++------ .../primitives/containers/primitive_result.py | 2 +- .../utils/estimator_result_decoder.py | 14 +++++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py b/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py index aab2b083d..48eb060d4 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py +++ b/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py @@ -24,7 +24,7 @@ class DataBinMeta(type): This is so that the class has a custom repr with DataBin<*shape> notation. """ - def __repr__(cls): + def __repr__(cls): # type: ignore[no-untyped-def] name = cls.__name__ if cls._SHAPE is None: return name @@ -45,7 +45,7 @@ class DataBin(metaclass=DataBinMeta): _FIELD_TYPES: tuple[type, ...] = () """The types of each field.""" - def __repr__(self): + def __repr__(self): # type: ignore[no-untyped-def] vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) return f"{type(self)}({', '.join(vals)})" @@ -76,7 +76,7 @@ def make_data_bin( unsafe_hash=True, repr=False, ) - cls._SHAPE = shape - cls._FIELDS = field_names - cls._FIELD_TYPES = field_types - return cls + cls._SHAPE = shape # type: ignore[attr-defined] + cls._FIELDS = field_names # type: ignore[attr-defined] + cls._FIELD_TYPES = field_types # type: ignore[no-untyped-def, attr-defined] + return cls # type: ignore[return-value] diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py b/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py index b383f4e11..9cce70715 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py +++ b/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py @@ -39,7 +39,7 @@ def metadata(self) -> dict[str, Any]: """The metadata of this primitive result.""" return self._metadata - def __getitem__(self, index) -> T: + def __getitem__(self, index: int) -> T: return self._pub_results[index] def __len__(self) -> int: diff --git a/qiskit_ibm_runtime/utils/estimator_result_decoder.py b/qiskit_ibm_runtime/utils/estimator_result_decoder.py index 904745530..875ad9a0c 100644 --- a/qiskit_ibm_runtime/utils/estimator_result_decoder.py +++ b/qiskit_ibm_runtime/utils/estimator_result_decoder.py @@ -18,7 +18,11 @@ from qiskit.primitives import EstimatorResult from .result_decoder import ResultDecoder -from ..qiskit.primitives.pub_result import PubResult + +# TODO replace with qiskit versions when released +from ..qiskit.primitives.containers.primitive_result import PrimitiveResult +from ..qiskit.primitives.containers.data_bin import make_data_bin +from ..qiskit.primitives.containers.pub_result import PubResult class EstimatorResultDecoder(ResultDecoder): @@ -35,10 +39,14 @@ def decode( # type: ignore # pylint: disable=arguments-differ for val, meta in zip(decoded["values"], decoded["metadata"]): if not isinstance(val, np.ndarray): val = np.asarray(val) + data_bin_cls = make_data_bin( + [("evs", np.ndarray), ("stds", np.ndarray)], shape=val.shape + ) out_results.append( - PubResult(data={"evs": val, "stds": meta.pop("standard_error")}, metadata=meta) + PubResult(data=data_bin_cls(val, meta.pop("standard_error")), metadata=meta) ) - return out_results + # TODO what metadata should be passed in to PrimitiveResult? + return PrimitiveResult(out_results, metadata=decoded["metadata"]) return EstimatorResult( values=np.asarray(decoded["values"]), metadata=decoded["metadata"], From e4dcf3a342670f2f630b0e2859836eaaee3b9243 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 22 Jan 2024 12:06:31 -0500 Subject: [PATCH 07/19] remove old pub_result file --- .../qiskit/primitives/pub_result.py | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/pub_result.py diff --git a/qiskit_ibm_runtime/qiskit/primitives/pub_result.py b/qiskit_ibm_runtime/qiskit/primitives/pub_result.py deleted file mode 100644 index 76145cbdc..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/pub_result.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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. -# TODO replace with file in qiskit/primitives/containers/pub_result.py - -""" -Base Pub class -""" - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True) -class PubResult: - """Result of pub.""" - - data: dict - metadata: dict From 78d87ad26119fc2d32f1d5796387e60f8343adc5 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 22 Jan 2024 22:44:09 -0500 Subject: [PATCH 08/19] add integration test --- test/integration/test_estimator_v2.py | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/integration/test_estimator_v2.py diff --git a/test/integration/test_estimator_v2.py b/test/integration/test_estimator_v2.py new file mode 100644 index 000000000..b9f0174d8 --- /dev/null +++ b/test/integration/test_estimator_v2.py @@ -0,0 +1,60 @@ +# 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. + +"""Integration tests for Estimator V2""" + +from qiskit.circuit.library import RealAmplitudes +from qiskit.quantum_info import SparsePauliOp + +# TODO replace with real versions from qiskit +from qiskit_ibm_runtime.qiskit.primitives.containers.primitive_result import PrimitiveResult +from qiskit_ibm_runtime.qiskit.primitives.containers.pub_result import PubResult +from qiskit_ibm_runtime.qiskit.primitives.containers.data_bin import DataBin + +from qiskit_ibm_runtime import EstimatorV2, Session +from ..decorators import run_integration_test +from ..ibm_test_case import IBMIntegrationTestCase + + +class TestEstimatorV2(IBMIntegrationTestCase): + """Integration tests for Estimator V2 Primitive.""" + + def setUp(self) -> None: + super().setUp() + self.backend = "ibmq_qasm_simulator" + + @run_integration_test + def test_estimator_v2_session(self, service): + """Verify correct results are returned""" + + psi1 = RealAmplitudes(num_qubits=2, reps=2) + psi2 = RealAmplitudes(num_qubits=2, reps=3) + + # pylint: disable=invalid-name + 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] + + with Session(service, self.backend) as session: + estimator = EstimatorV2(session=session) + + job = estimator.run([(psi1, [H1, H3], [theta1, theta3]), (psi2, H2, theta2)]) + result = job.result() + self.assertIsInstance(result, PrimitiveResult) + self.assertIsInstance(result[0], PubResult) + self.assertIsInstance(result[0].data, DataBin) + self.assertEqual(len(result[0].data.evs), 2) + self.assertEqual(len(result[0].data.stds), 2) From ebe74e3f7c80f8d8d482ad4a61f22a2796d92e9a Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 23 Jan 2024 13:19:20 -0500 Subject: [PATCH 09/19] use qiskit main & remove copied files --- qiskit_ibm_runtime/base_primitive.py | 3 +- qiskit_ibm_runtime/estimator.py | 10 +- .../qiskit/primitives/__init__.py | 1 - .../primitives/base/base_primitive_job.py | 78 ----------- .../qiskit/primitives/base/base_result.py | 87 ------------ .../qiskit/primitives/base_estimator.py | 132 ------------------ .../qiskit/primitives/containers/data_bin.py | 82 ----------- .../primitives/containers/primitive_result.py | 52 ------- .../primitives/containers/pub_result.py | 45 ------ .../utils/estimator_result_decoder.py | 6 +- qiskit_ibm_runtime/utils/json.py | 5 +- requirements.txt | 2 +- setup.py | 2 +- test/integration/test_estimator_v2.py | 5 +- test/unit/test_data_serialization.py | 5 +- 15 files changed, 12 insertions(+), 503 deletions(-) delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base/base_result.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base_estimator.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 77312a69a..adc1b2b91 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -20,6 +20,7 @@ from dataclasses import asdict, replace import warnings +from qiskit.primitives import EstimatorPub, SamplerPub from qiskit.providers.options import Options as TerraOptions from qiskit_ibm_provider.session import get_cm_session as get_cm_provider_session @@ -33,8 +34,6 @@ from .constants import DEFAULT_DECODERS from .qiskit_runtime_service import QiskitRuntimeService -# TODO: remove when we have real v2 base estimator -from .qiskit.primitives import EstimatorPub, SamplerPub # pylint: disable=unused-import,cyclic-import from .session import Session diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index ce497ab96..757dd5ab1 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -21,6 +21,8 @@ from qiskit.circuit import QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.primitives import BaseEstimator +from qiskit.primitives.base import BaseEstimatorV2 +from qiskit.primitives.containers import EstimatorPub, EstimatorPubLike from .runtime_job import RuntimeJob from .ibm_backend import IBMBackend @@ -30,10 +32,6 @@ from .utils.qctrl import validate as qctrl_validate -# TODO: remove when we have real v2 base estimator, estimatorpub, and estimatorpublike -from .qiskit.primitives import BaseEstimatorV2 -from .qiskit.primitives.estimator_pub import EstimatorPub, EstimatorPubLike - # pylint: disable=unused-import,cyclic-import from .session import Session @@ -133,7 +131,7 @@ def __init__( raise NotImplementedError("EstimatorV2 is not supported with q-ctrl channel strategy.") def run( - self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike], precision: float | None = None + self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike], *, precision: float | None = None ) -> RuntimeJob: """Submit a request to the estimator primitive. @@ -156,7 +154,7 @@ def run( for pub in pubs: pub.validate() # type: ignore[union-attr] - return self._run(pubs) + return self._run(pubs) # type: ignore[arg-type] def _validate_options(self, options: dict) -> None: """Validate that program inputs (options) are valid diff --git a/qiskit_ibm_runtime/qiskit/primitives/__init__.py b/qiskit_ibm_runtime/qiskit/primitives/__init__.py index 2ca4c26e6..e70171bbc 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/__init__.py +++ b/qiskit_ibm_runtime/qiskit/primitives/__init__.py @@ -12,7 +12,6 @@ """Temporary copy of base primitives""" -from .base_estimator import BaseEstimatorV2 # type: ignore[attr-defined] from .base_sampler import BaseSamplerV2 # type: ignore[attr-defined] from .bindings_array import BindingsArray # type: ignore[attr-defined] from .observables_array import ObservablesArray # type: ignore[attr-defined] diff --git a/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py b/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py deleted file mode 100644 index 9dca27c0c..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base/base_primitive_job.py +++ /dev/null @@ -1,78 +0,0 @@ -# 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. -""" -Primitive job abstract base class -""" - -from abc import ABC, abstractmethod -from typing import Generic, TypeVar, Union - -from ..containers.primitive_result import PrimitiveResult -from .base_result import BasePrimitiveResult - -ResultT = TypeVar("ResultT", bound=Union[BasePrimitiveResult, PrimitiveResult]) -StatusT = TypeVar("StatusT") - - -class BasePrimitiveJob(ABC, Generic[ResultT, StatusT]): - """Primitive job abstract base class.""" - - def __init__(self, job_id: str, **kwargs) -> None: - """Initializes the primitive job. - - Args: - job_id: A unique id in the context of the primitive used to run the job. - kwargs: Any key value metadata to associate with this job. - """ - self._job_id = job_id - self.metadata = kwargs - - def job_id(self) -> str: - """Return a unique id identifying the job.""" - return self._job_id - - @abstractmethod - def result(self) -> ResultT: - """Return the results of the job.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `result` method.") - - @abstractmethod - def status(self) -> StatusT: - """Return the status of the job.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `status` method.") - - @abstractmethod - def done(self) -> bool: - """Return whether the job has successfully run.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `done` method.") - - @abstractmethod - def running(self) -> bool: - """Return whether the job is actively running.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `running` method.") - - @abstractmethod - def cancelled(self) -> bool: - """Return whether the job has been cancelled.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `cancelled` method.") - - @abstractmethod - def in_final_state(self) -> bool: - """Return whether the job is in a final job state such as ``DONE`` or ``ERROR``.""" - raise NotImplementedError( - "Subclass of BasePrimitiveJob must implement `is_final_state` method." - ) - - @abstractmethod - def cancel(self): - """Attempt to cancel the job.""" - raise NotImplementedError("Subclass of BasePrimitiveJob must implement `cancel` method.") diff --git a/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py b/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py deleted file mode 100644 index 2c23ea1d7..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base/base_result.py +++ /dev/null @@ -1,87 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Primitive result abstract base class -""" - -from __future__ import annotations - -from abc import ABC -from collections.abc import Iterator, Sequence -from dataclasses import fields -from typing import Any, Dict - -from numpy import ndarray - - -ExperimentData = Dict[str, Any] - - -class BasePrimitiveResult(ABC): - """Primitive result abstract base class. - - Base class for Primitive results meant to provide common functionality to all inheriting - result dataclasses. - """ - - def __post_init__(self) -> None: - """ - Verify that all fields in any inheriting result dataclass are consistent, after - instantiation, with the number of experiments being represented. - - This magic method is specific of `dataclasses.dataclass`, therefore all inheriting - classes must have this decorator. - - Raises: - TypeError: If one of the data fields is not a Sequence or ``numpy.ndarray``. - ValueError: Inconsistent number of experiments across data fields. - """ - for value in self._field_values: # type: Sequence - # TODO: enforce all data fields to be tuples instead of sequences - if not isinstance(value, (Sequence, ndarray)) or isinstance(value, (str, bytes)): - raise TypeError( - f"Expected sequence or `numpy.ndarray`, provided {type(value)} instead." - ) - if len(value) != self.num_experiments: - raise ValueError("Inconsistent number of experiments across data fields.") - - @property # TODO: functools.cached_property when py37 is droppped - def num_experiments(self) -> int: - """Number of experiments in any inheriting result dataclass.""" - value: Sequence = self._field_values[0] - return len(value) - - @property # TODO: functools.cached_property when py37 is droppped - def experiments(self) -> tuple[ExperimentData, ...]: - """Experiment data dicts in any inheriting result dataclass.""" - return tuple(self._generate_experiments()) - - def _generate_experiments(self) -> Iterator[ExperimentData]: - """Generate experiment data dicts in any inheriting result dataclass.""" - names: tuple[str, ...] = self._field_names - for values in zip(*self._field_values): - yield dict(zip(names, values)) - - def decompose(self) -> Iterator[BasePrimitiveResult]: - """Generate single experiment result objects from self.""" - for values in zip(*self._field_values): - yield self.__class__(*[(v,) for v in values]) - - @property # TODO: functools.cached_property when py37 is droppped - def _field_names(self) -> tuple[str, ...]: - """Tuple of field names in any inheriting result dataclass.""" - return tuple(field.name for field in fields(self)) - - @property # TODO: functools.cached_property when py37 is droppped - def _field_values(self) -> tuple: - """Tuple of field values in any inheriting result dataclass.""" - return tuple(getattr(self, name) for name in self._field_names) diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py deleted file mode 100644 index 33ff44a75..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py +++ /dev/null @@ -1,132 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# type: ignore - -r""" - -.. estimator-desc: - -======================== -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}") -""" - -from __future__ import annotations - -from abc import abstractmethod -from typing import TypeVar, Iterable - -from qiskit.providers import JobV1 as Job - -import numpy as np -from numpy.typing import NDArray - -from .estimator_pub import EstimatorPub, EstimatorPubLike - -# TODO remove this after released in qiskit -from .containers.data_bin import DataBin, make_data_bin -from .containers.primitive_result import PrimitiveResult -from .containers.pub_result import PubResult -from .base.base_primitive_job import BasePrimitiveJob - -T = TypeVar("T", bound=Job) # pylint: disable=invalid-name - - -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_ibm_runtime/qiskit/primitives/containers/data_bin.py b/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py deleted file mode 100644 index 48eb060d4..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/containers/data_bin.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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. - -""" -Dataclass tools for data namespaces (bins) -""" -from __future__ import annotations - -from collections.abc import Iterable -from dataclasses import make_dataclass - - -class DataBinMeta(type): - """Metaclass for :class:`DataBin` that adds the shape to the type name. - This is so that the class has a custom repr with DataBin<*shape> notation. - """ - - def __repr__(cls): # type: ignore[no-untyped-def] - name = cls.__name__ - if cls._SHAPE is None: - return name - shape = ",".join(map(str, cls._SHAPE)) - return f"{name}<{shape}>" - - -class DataBin(metaclass=DataBinMeta): - """Base class for data bin containers. - Subclasses are typically made via :class:`~make_data_bin`, which is a specialization of - :class:`make_dataclass`. - """ - - _RESTRICTED_NAMES = ("_RESTRICTED_NAMES", "_SHAPE", "_FIELDS", "_FIELD_TYPES") - _SHAPE: tuple[int, ...] | None = None - _FIELDS: tuple[str, ...] = () - """The fields allowed in this data bin.""" - _FIELD_TYPES: tuple[type, ...] = () - """The types of each field.""" - - def __repr__(self): # type: ignore[no-untyped-def] - vals = (f"{name}={getattr(self, name)}" for name in self._FIELDS if hasattr(self, name)) - return f"{type(self)}({', '.join(vals)})" - - -def make_data_bin( - fields: Iterable[tuple[str, type]], shape: tuple[int, ...] | None = None -) -> DataBinMeta: - """Return a new subclass of :class:`~DataBin` with the provided fields and shape. - .. code-block:: python - my_bin = make_data_bin([("alpha", np.NDArray[np.float])], shape=(20, 30)) - # behaves like a dataclass - my_bin(alpha=np.empty((20, 30))) - Args: - fields: Tuples ``(name, type)`` specifying the attributes of the returned class. - shape: The intended shape of every attribute of this class. - Returns: - A new class. - """ - field_names, field_types = zip(*fields) - for name in field_names: - if name in DataBin._RESTRICTED_NAMES: - raise ValueError(f"'{name}' is a restricted name for a DataBin.") - cls = make_dataclass( - "DataBin", - dict(zip(field_names, field_types)), - bases=(DataBin,), - frozen=True, - unsafe_hash=True, - repr=False, - ) - cls._SHAPE = shape # type: ignore[attr-defined] - cls._FIELDS = field_names # type: ignore[attr-defined] - cls._FIELD_TYPES = field_types # type: ignore[no-untyped-def, attr-defined] - return cls # type: ignore[return-value] diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py b/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py deleted file mode 100644 index 9cce70715..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/containers/primitive_result.py +++ /dev/null @@ -1,52 +0,0 @@ -# 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. - -"""PrimitiveResult""" - -from __future__ import annotations - -from collections.abc import Iterable -from typing import Any, Generic, TypeVar - -from .pub_result import PubResult - -T = TypeVar("T", bound=PubResult) - - -class PrimitiveResult(Generic[T]): - """A container for multiple pub results and global metadata.""" - - def __init__(self, pub_results: Iterable[T], metadata: dict[str, Any] | None = None): - """ - Args: - pub_results: Pub results. - metadata: Any metadata that doesn't make sense to put inside of pub results. - """ - self._pub_results = list(pub_results) - self._metadata = metadata or {} - - @property - def metadata(self) -> dict[str, Any]: - """The metadata of this primitive result.""" - return self._metadata - - def __getitem__(self, index: int) -> T: - return self._pub_results[index] - - def __len__(self) -> int: - return len(self._pub_results) - - def __repr__(self) -> str: - return f"PrimitiveResult({self._pub_results}, metadata={self.metadata})" - - def __iter__(self) -> Iterable[T]: - return iter(self._pub_results) diff --git a/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py b/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py deleted file mode 100644 index c7ac2b616..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/containers/pub_result.py +++ /dev/null @@ -1,45 +0,0 @@ -# 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. - -""" -Base Pub class -""" - -from __future__ import annotations - -from .data_bin import DataBin - - -class PubResult: - """Result of Primitive Unified Bloc.""" - - __slots__ = ("_data", "_metadata") - - def __init__(self, data: DataBin, metadata: dict | None = None): - """Initialize a pub result. - - Args: - data: result data bin. - metadata: metadata dictionary. - """ - self._data = data - self._metadata = metadata or {} - - @property - def data(self) -> DataBin: - """Result data for the pub.""" - return self._data - - @property - def metadata(self) -> dict: - """Metadata for the pub.""" - return self._metadata diff --git a/qiskit_ibm_runtime/utils/estimator_result_decoder.py b/qiskit_ibm_runtime/utils/estimator_result_decoder.py index 875ad9a0c..513ec984b 100644 --- a/qiskit_ibm_runtime/utils/estimator_result_decoder.py +++ b/qiskit_ibm_runtime/utils/estimator_result_decoder.py @@ -16,14 +16,10 @@ import numpy as np from qiskit.primitives import EstimatorResult +from qiskit.primitives.containers import PrimitiveResult, make_data_bin, PubResult from .result_decoder import ResultDecoder -# TODO replace with qiskit versions when released -from ..qiskit.primitives.containers.primitive_result import PrimitiveResult -from ..qiskit.primitives.containers.data_bin import make_data_bin -from ..qiskit.primitives.containers.pub_result import PubResult - class EstimatorResultDecoder(ResultDecoder): """Class used to decode estimator results""" diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index f6ba10c00..52025f4f1 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -53,6 +53,7 @@ QuantumRegister, ) from qiskit.circuit.parametertable import ParameterView +from qiskit.primitives import ObservablesArray, BindingsArray, EstimatorPub from qiskit.result import Result from qiskit.version import __version__ as _terra_version_string @@ -65,10 +66,6 @@ dump, load, ) -from qiskit_ibm_runtime.qiskit.primitives.estimator_pub import EstimatorPub - -# TODO: Remove when they are in terra -from ..qiskit.primitives import ObservablesArray, BindingsArray _TERRA_VERSION = tuple( int(x) for x in re.match(r"\d+\.\d+\.\d", _terra_version_string).group(0).split(".")[:3] diff --git a/requirements.txt b/requirements.txt index 3697f71fd..0b23416ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -qiskit>=0.44.1 +qiskit @ git+https://github.com/Qiskit/qiskit.git requests~=2.27 requests_ntlm>=1.1.0 numpy>=1.13 diff --git a/setup.py b/setup.py index 5bd6e41d4..ac735c079 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ import setuptools REQUIREMENTS = [ - "qiskit>=0.44.1", + "qiskit @ git+https://github.com/Qiskit/qiskit.git", "requests>=2.19", "requests-ntlm>=1.1.0", "numpy>=1.13", diff --git a/test/integration/test_estimator_v2.py b/test/integration/test_estimator_v2.py index b9f0174d8..a4a4198b7 100644 --- a/test/integration/test_estimator_v2.py +++ b/test/integration/test_estimator_v2.py @@ -15,10 +15,7 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -# TODO replace with real versions from qiskit -from qiskit_ibm_runtime.qiskit.primitives.containers.primitive_result import PrimitiveResult -from qiskit_ibm_runtime.qiskit.primitives.containers.pub_result import PubResult -from qiskit_ibm_runtime.qiskit.primitives.containers.data_bin import DataBin +from qiskit.primitives.containers import PrimitiveResult, PubResult, DataBin from qiskit_ibm_runtime import EstimatorV2, Session from ..decorators import run_integration_test diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index 758f62520..96fc83f9b 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -23,18 +23,17 @@ import numpy as np from ddt import data, ddt +import qiskit.quantum_info as qi from qiskit.circuit import Parameter, QuantumCircuit from qiskit.test.reference_circuits import ReferenceCircuits from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate from qiskit.providers.fake_provider import FakeNairobi -import qiskit.quantum_info as qi from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector from qiskit.result import Result +from qiskit.primitives import BindingsArray, ObservablesArray from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder -# TODO: Remove when they are in terra -from qiskit_ibm_runtime.qiskit.primitives import BindingsArray, ObservablesArray from .mock.fake_runtime_client import CustomResultRuntimeJob from .mock.fake_runtime_service import FakeRuntimeService From fb0c5ea5ffd247bd9fe6a731114b5c4d76d1c781 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 23 Jan 2024 13:39:29 -0500 Subject: [PATCH 10/19] fix lint, remove more copied files --- .../qiskit/primitives/__init__.py | 4 - .../qiskit/primitives/base_sampler.py | 2 +- .../qiskit/primitives/bindings_array.py | 439 ------------------ .../qiskit/primitives/estimator_pub.py | 186 -------- .../qiskit/primitives/observables_array.py | 267 ----------- .../qiskit/primitives/sampler_pub.py | 151 ------ qiskit_ibm_runtime/utils/json.py | 17 +- test/unit/test_estimator.py | 2 +- test/unit/test_ibm_primitives_v2.py | 3 +- test/unit/test_sampler.py | 2 +- 10 files changed, 18 insertions(+), 1055 deletions(-) delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/bindings_array.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/observables_array.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py diff --git a/qiskit_ibm_runtime/qiskit/primitives/__init__.py b/qiskit_ibm_runtime/qiskit/primitives/__init__.py index e70171bbc..35bf28961 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/__init__.py +++ b/qiskit_ibm_runtime/qiskit/primitives/__init__.py @@ -13,7 +13,3 @@ """Temporary copy of base primitives""" from .base_sampler import BaseSamplerV2 # type: ignore[attr-defined] -from .bindings_array import BindingsArray # type: ignore[attr-defined] -from .observables_array import ObservablesArray # type: ignore[attr-defined] -from .estimator_pub import EstimatorPub # type: ignore[attr-defined] -from .sampler_pub import SamplerPub # type: ignore[attr-defined] diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py b/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py index 7440c3f18..74f5e225f 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py +++ b/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py @@ -81,10 +81,10 @@ from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job +from qiskit.primitives.containers import SamplerPub, SamplerPubLike from .base_primitive import BasePrimitiveV2 from .options import BasePrimitiveOptionsLike -from .sampler_pub import SamplerPub, SamplerPubLike T = TypeVar("T", bound=Job) # pylint: disable=invalid-name diff --git a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py deleted file mode 100644 index 6f84ca46f..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py +++ /dev/null @@ -1,439 +0,0 @@ -# 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. - -# Copied from qiskit, should be removed after first release -# mypy: ignore-errors -""" -Bindings array class -""" -from __future__ import annotations - -from collections.abc import Iterable, Mapping, Sequence -from itertools import chain, islice -from typing import Union - -import numpy as np -from numpy.typing import ArrayLike - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.parameterexpression import ParameterValueType - -from .shape import ShapedMixin, ShapeInput, shape_tuple - -ParameterLike = Union[Parameter, str] - - -class BindingsArray(ShapedMixin): - r"""Stores parameter binding value sets for a :class:`qiskit.QuantumCircuit`. - - A single parameter binding set provides numeric values to bind to a circuit with free - :class:`qiskit.circuit.Parameter`\s. An instance of this class stores an array-valued - collection of such sets. The simplest example is a 0-d array consisting of a single - parameter binding set, whereas an n-d array of parameter binding sets represents an - n-d sweep over values. - - The storage format is a list of arrays, ``[vals0, vals1, ...]``, as well as a dictionary of - arrays attached to parameters, ``{params0: kwvals0, ...}``. A convention is used - where the last dimension of each array indexes (a subset of) circuit parameters. For - example, if the last dimension of ``vals1`` is 25, then it represents an array of - possible binding values for 25 distinct parameters, where its leading shape is the - array :attr:`~.shape` of its binding array. This implies a degeneracy of the storage - format: ``[vals, vals1[..., :10], vals1[..., 10:], ...]`` is exactly equivalent to - ``[vals0, vals1, ...]`` in the bindings it specifies. This allows flexibility about whether - values for different parameters are stored in one big array, or across several smaller - arrays. It also allows different parameters to use different dtypes. - - .. code-block:: python - - # 0-d array (i.e. only one binding) - BindingsArray([1, 2, 3], {"a": 4, ("b", "c"): [5, 6]}) - - # single array, last index is parameters - BindingsArray(np.empty((10, 10, 100))) - - # multiple arrays, where each last index is parameters. notice that it's smart enough to - # figure out that a missing last dimension corresponds to a single parameter. - BindingsArray( - [np.empty((10, 10, 100)), np.empty((10, 10)), np.empty((10, 10, 20), dtype=complex)], - {("c", "a"): np.empty((10, 10, 2)), "b": np.empty((10, 10))} - ) - """ - __slots__ = ("_vals", "_kwvals") - - def __init__( - self, - vals: ArrayLike | Iterable[ArrayLike] | None = None, - kwvals: Mapping[ParameterLike, Iterable[ParameterValueType]] | ArrayLike | None = None, - shape: ShapeInput | None = None, - ): - r""" - Initialize a ``BindingsArray``. It can take parameter vectors and dictionaries. - - The ``shape`` argument does not need to be provided whenever it can unambiguously - be inferred from the provided arrays. Ambiguity arises because an array provided to the - constructor might represent values for either a single parameter, with an implicit missing - last dimension of size ``1``, or for many parameters, where the size of the last dimension - is the number of parameters it is providing values to. This ambiguity can be broken in the - following common ways: - - * Only a single array is provided to ``vals``, and no arrays to ``kwvals``, in which case - it is assumed that the last dimension is over many parameters. - * Multiple arrays are given whose shapes differ only in the last dimension size. - * Some array is given in ``kwvals`` where the key contains multiple - :class:`~.Parameter`\s, whose length the last dimension of the array must therefore match. - - Args: - vals: One or more arrays, where the last index of each corresponds to - distinct parameters. If their dtypes allow it, concatenating these - arrays over the last axis is equivalent to providing them separately. - kwvals: A mapping from one or more parameters to arrays of values to bind - them to, where the last axis is over parameters. - shape: The leading shape of every array in these bindings. - - Raises: - ValueError: If all inputs are ``None``. - ValueError: If the shape cannot be automatically inferred from the arrays, or if there - is some inconsistency in the shape of the given arrays. - """ - super().__init__() - - if vals is None: - vals = [] - if kwvals is None: - kwvals = {} - - vals = [vals] if isinstance(vals, np.ndarray) else [np.array(v, copy=False) for v in vals] - kwvals = { - _format_key((p,)) - if isinstance(p, Parameter) - else _format_key(p): np.array(val, copy=False) - for p, val in kwvals.items() - } - - if shape is None: - # jump through hoops to find out user's intended shape - shape = _infer_shape(vals, kwvals) - - # shape checking, and normalization so that each last index must be over parameters - self._shape = shape_tuple(shape) - for idx, val in enumerate(vals): - vals[idx] = _standardize_shape(val, self._shape) - - self._vals: list[np.ndarray] = vals - self._kwvals = kwvals - - self.validate() - - def __getitem__(self, args) -> BindingsArray: - # because the parameters live on the last axis, we don't need to do anything special to - # accomodate them because there will always be an implicit slice(None, None, None) - # on all unspecified trailing dimensions - # separately, we choose to not disallow args which touch the last dimension, even though it - # would not be a particularly friendly way to chop parameters - vals = [val[args] for val in self._vals] - kwvals = {params: val[args] for params, val in self._kwvals.items()} - try: - shape = next(chain(vals, kwvals.values())).shape[:-1] - except StopIteration: - shape = () - return BindingsArray(vals, kwvals, shape) - - def __repr__(self): - descriptions = [f"shape={self.shape}", f"num_parameters={self.num_parameters}"] - if num_kwval_params := sum(val.shape[-1] for val in self._kwvals.values()): - names = list(islice(map(repr, chain.from_iterable(map(_format_key, self._kwvals))), 5)) - if len(names) < num_kwval_params: - names.append("...") - descriptions.append(f"parameters=[{', '.join(names)}]") - return f"{type(self).__name__}(<{', '.join(descriptions)}>)" - - @property - def kwvals(self) -> dict[tuple[str, ...], np.ndarray]: - """The keyword values of this array.""" - return self._kwvals - - @property - def num_parameters(self) -> int: - """The total number of parameters.""" - return sum(val.shape[-1] for val in chain(self.vals, self._kwvals.values())) - - @property - def vals(self) -> list[np.ndarray]: - """The non-keyword values of this array.""" - return self._vals - - def as_array(self, parameters: Iterable[Parameter | str] | None = None) -> np.ndarray: - """Return the contents of this bindings array as a single NumPy array. - - As with each :attr:`~vals` and :attr:`~kwvals` array, the parameters are indexed along the - last dimension. - - The order of the :attr:`~vals` portion of this bindings array is always preserved, and - always comes first in the returned array, irrespective of whether ``parameters`` are - provided. - - If ``parameters`` are provided, then they determine the order of any :attr:`~kwvals` - present in this bindings array. If :attr:`~vals` are present in addition to :attr:`~kwvals`, - then they appear before the :attr:`~kwvals` always. - - Parameters: - parameters: Optional parameters that determine the order of the output. - - Returns: - This bindings array as a single NumPy array. - - Raises: - RuntimeError: If these bindings contain multiple dtypes. - ValueError: If ``parameters`` are provided, but do not match those found in ``kwvals``. - """ - dtypes = {arr.dtype for arr in self.vals} - dtypes.update(arr.dtype for arr in self.kwvals.values()) - if len(dtypes) > 1: - raise RuntimeError(f"Multiple dtypes ({dtypes}) were found.") - dtype = next(iter(dtypes)) if dtypes else float - - if self.num_parameters == 0 and not self.shape: - # we want this special case to look like a single binding on no parameters - return np.empty((1, 0), dtype=dtype) - - ret = np.empty(shape_tuple(self.shape, self.num_parameters), dtype=dtype) - - # always start by placing the vals in the returned array - pos = 0 - for arr in self.vals: - size = arr.shape[-1] - ret[..., pos : pos + size] = arr - pos += size - - if parameters is None: - # preserve the order of the kwvals - for arr in self.kwvals.values(): - size = arr.shape[-1] - ret[..., pos : pos + size] = arr - pos += size - elif self.kwvals: - # use the order of the provided parameters - parameters = list(parameters) - if len(parameters) != (num_kwval := sum(arr.shape[-1] for arr in self.kwvals.values())): - raise ValueError(f"Expected {num_kwval} parameters but {len(parameters)} received.") - - idx_lookup = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)} - for arr_params, arr in self.kwvals.items(): - try: - idxs = [idx_lookup[_param_name(param)] + pos for param in arr_params] - except KeyError as ex: - missing = next(p for p in map(_param_name, arr_params) if p not in idx_lookup) - raise ValueError(f"Could not find placement for parameter '{missing}'.") from ex - ret[..., idxs] = arr - - return ret - - def bind(self, circuit: QuantumCircuit, loc: tuple[int, ...]) -> QuantumCircuit: - """Return a new circuit bound to the values at the provided index. - - Args: - circuit: The circuit to bind. - loc: A tuple of indices, on for each dimension of this array. - - Returns: - The bound circuit. - - Raises: - ValueError: If the index doesn't have the right number of values. - """ - if len(loc) != self.ndim: - raise ValueError(f"Expected {loc} to index all dimensions of {self.shape}") - - flat_vals = (val for vals in self.vals for val in vals[loc]) - - if not self._kwvals: - # special case to avoid constructing a dictionary input - return circuit.assign_parameters(list(flat_vals)) - - parameters = dict(zip(circuit.parameters, flat_vals)) - parameters.update( - (param, val) - for params, vals in self._kwvals.items() - for param, val in zip(params, vals[loc]) - ) - return circuit.assign_parameters(parameters) - - def bind_all(self, circuit: QuantumCircuit) -> np.ndarray: - """Return an object array of bound circuits with the same shape. - - Args: - circuit: The circuit to bind. - - Returns: - An object array of the same shape containing all bound circuits. - """ - arr = np.empty(self.shape, dtype=object) - for idx in np.ndindex(self.shape): - arr[idx] = self.bind(circuit, idx) - return arr - - def ravel(self) -> BindingsArray: - """Return a new :class:`~BindingsArray` with one dimension. - - The returned bindings array has a :attr:`shape` given by ``(size, )``, where the size is the - :attr:`~size` of this bindings array. - - Returns: - A new bindings array. - """ - return self.reshape(self.size) - - def reshape(self, shape: int | Iterable[int]) -> BindingsArray: - """Return a new :class:`~BindingsArray` with a different shape. - - This results in a new view of the same arrays. - - Args: - shape: The shape of the returned bindings array. - - Returns: - A new bindings array. - - Raises: - ValueError: If the provided shape has a different product than the current size. - """ - shape = (shape, -1) if isinstance(shape, int) else (*shape, -1) - if np.prod(shape[:-1]).astype(int) != self.size: - raise ValueError("Reshaping cannot change the total number of elements.") - vals = [val.reshape(shape) for val in self._vals] - kwvals = {params: val.reshape(shape) for params, val in self._kwvals.items()} - return BindingsArray(vals, kwvals, shape[:-1]) - - @classmethod - def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray: - """Coerce an input that is :class:`~BindingsArrayLike` into a new :class:`~BindingsArray`. - - Args: - bindings_array: An object to be bindings array. - - Returns: - A new bindings array. - """ - # TODO added this - if the value passed in is already a BindingsArray, - # it should be returned instead of throwing a type error right? - if isinstance(bindings_array, BindingsArray): - return bindings_array - if isinstance(bindings_array, Sequence): - bindings_array = np.array(bindings_array) - if bindings_array is None: - bindings_array = cls() - elif isinstance(bindings_array, np.ndarray): - bindings_array = cls(bindings_array) - elif isinstance(bindings_array, Mapping): - bindings_array = cls(kwvals=bindings_array) - else: - raise TypeError(f"Unsupported type {type(bindings_array)} is given.") - return bindings_array - - def validate(self): - """Validate the consistency in bindings_array.""" - for parameters, val in self._kwvals.items(): - val = self._kwvals[parameters] = _standardize_shape(val, self._shape) - if len(parameters) != val.shape[-1]: - raise ValueError( - f"Length of {parameters} inconsistent with last dimension of {val}" - ) - - -def _standardize_shape(val: np.ndarray, shape: tuple[int, ...]) -> np.ndarray: - """Return ``val`` or ``val[..., None]``. - - Args: - val: The array whose shape to standardize. - shape: The shape to standardize to. - - Returns: - An array with one more dimension than ``len(shape)``, and whose leading dimensions match - ``shape``. - - Raises: - ValueError: If the leading shape of ``val`` does not match the ``shape``. - """ - if val.shape == shape: - val = val[..., None] - elif val.ndim - 1 != len(shape) or val.shape[:-1] != shape: - raise ValueError(f"Array with shape {val.shape} inconsistent with {shape}") - return val - - -def _infer_shape( - vals: list[np.ndarray], kwvals: dict[tuple[Parameter, ...], np.ndarray] -) -> tuple[int, ...]: - """Return a shape tuple that consistently defines the leading dimensions of all arrays. - - Args: - vals: A list of arrays. - kwvals: A mapping from tuples to arrays, where the length of each tuple should match the - last dimension of the corresponding array. - - Returns: - A shape tuple that matches the leading dimension of every array. - - Raises: - ValueError: If this cannot be done unambiguously. - """ - only_possible_shapes = None - - def examine_array(*possible_shapes): - nonlocal only_possible_shapes - if only_possible_shapes is None: - only_possible_shapes = set(possible_shapes) - else: - only_possible_shapes.intersection_update(possible_shapes) - - for parameters, val in kwvals.items(): - if len(parameters) > 1: - # here, the last dimension _has_ to be over parameters - examine_array(val.shape[:-1]) - elif val.shape == () or val.shape == (1,) or val.shape[-1] != 1: - # here, if the last dimension is not 1 or shape is () or (1,) then the shape is the shape - examine_array(val.shape) - else: - # here, the last dimension could be over parameters or not - examine_array(val.shape, val.shape[:-1]) - - if len(vals) == 1 and len(kwvals) == 0: - examine_array(vals[0].shape[:-1]) - elif len(vals) == 0 and len(kwvals) == 0: - examine_array(()) - else: - for val in vals: - # here, the last dimension could be over parameters or not - examine_array(val.shape, val.shape[:-1]) - - if len(only_possible_shapes) == 1: - return next(iter(only_possible_shapes)) - elif len(only_possible_shapes) == 0: - raise ValueError("Could not find any consistent shape.") - raise ValueError("Could not unambiguously determine the intended shape; specify shape manually") - - -def _format_key(key: tuple[Parameter | str, ...]): - return tuple(map(_param_name, key)) - - -def _param_name(param: Parameter | str) -> str: - return param.name if isinstance(param, Parameter) else param - - -BindingsArrayLike = Union[ - BindingsArray, - ArrayLike, - "Mapping[Parameter, ArrayLike]", - "Sequence[ArrayLike]", - None, -] diff --git a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py b/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py deleted file mode 100644 index d273e9990..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py +++ /dev/null @@ -1,186 +0,0 @@ -# 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. - -# Copied from qiskit, should be removed after first release -# mypy: ignore-errors -""" -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_ibm_runtime/qiskit/primitives/observables_array.py b/qiskit_ibm_runtime/qiskit/primitives/observables_array.py deleted file mode 100644 index 981b6ef5f..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py +++ /dev/null @@ -1,267 +0,0 @@ -# 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. - -# Copied from qiskit, should be removed after first release -# mypy: ignore-errors - -""" -ND-Array container class for Estimator observables. -""" -from __future__ import annotations - -import re -from collections import defaultdict -from collections.abc import Mapping as MappingType -from functools import lru_cache -from typing import Iterable, Mapping, Union, overload - -import numpy as np -from numpy.typing import ArrayLike - -from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp - -from .object_array import object_array -from .shape import ShapedMixin - -BasisObservable = Mapping[str, complex] -"""Representation type of a single observable.""" - -BasisObservableLike = Union[ - str, - Pauli, - SparsePauliOp, - Mapping[Union[str, Pauli], complex], - Iterable[Union[str, Pauli, SparsePauliOp]], -] -"""Types that can be natively used to construct a :const:`BasisObservable`.""" - - -class ObservablesArray(ShapedMixin): - """An ND-array of :const:`.BasisObservable` for an :class:`.Estimator` primitive.""" - - __slots__ = ("_array", "_shape") - ALLOWED_BASIS: str = "IXYZ01+-lr" - """The allowed characters in :const:`BasisObservable` strings.""" - - def __init__( - self, - observables: BasisObservableLike | ArrayLike, - copy: bool = True, - validate: bool = True, - ): - """Initialize an observables array. - - Args: - observables: An array-like of basis observable compatible objects. - copy: Specify the ``copy`` kwarg of the :func:`.object_array` function - when initializing observables. - validate: If True, convert :const:`.BasisObservableLike` input objects - to :const:`.BasisObservable` objects and validate. If False the - input should already be an array-like of valid - :const:`.BasisObservble` objects. - - Raises: - ValueError: If ``validate=True`` and the input observables is not valid. - """ - super().__init__() - if isinstance(observables, ObservablesArray): - observables = observables._array - self._array = object_array(observables, copy=copy, list_types=(PauliList,)) - self._shape = self._array.shape - if validate: - num_qubits = None - for ndi, obs in np.ndenumerate(self._array): - basis_obs = self.format_observable(obs) - basis_num_qubits = len(next(iter(basis_obs))) - if num_qubits is None: - num_qubits = basis_num_qubits - elif basis_num_qubits != num_qubits: - raise ValueError( - "The number of qubits must be the same for all observables in the " - "observables array." - ) - self._array[ndi] = basis_obs - - def __repr__(self): - prefix = f"{type(self).__name__}(" - suffix = f", shape={self.shape})" - array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50) - return prefix + array + suffix - - def tolist(self) -> list: - """Convert to a nested list""" - return self._array.tolist() - - def __array__(self, dtype=None): - """Convert to an Numpy.ndarray""" - if dtype is None or dtype == object: - return self._array - raise ValueError("Type must be 'None' or 'object'") - - @overload - def __getitem__(self, args: int | tuple[int, ...]) -> BasisObservable: - ... - - @overload - def __getitem__(self, args: slice) -> ObservablesArray: - ... - - def __getitem__(self, args): - item = self._array[args] - if not isinstance(item, np.ndarray): - return item - return ObservablesArray(item, copy=False, validate=False) - - def reshape(self, shape: int | Iterable[int]) -> ObservablesArray: - """Return a new array with a different shape. - - This results in a new view of the same arrays. - - Args: - shape: The shape of the returned array. - - Returns: - A new array. - """ - return ObservablesArray(self._array.reshape(shape), copy=False, validate=False) - - def ravel(self) -> ObservablesArray: - """Return a new array with one dimension. - - The returned array has a :attr:`shape` given by ``(size, )``, where - the size is the :attr:`~size` of this array. - - Returns: - A new flattened array. - """ - return self.reshape(self.size) - - @classmethod - def format_observable(cls, observable: BasisObservableLike) -> BasisObservable: - """Format an observable-like object into a :const:`BasisObservable`. - - Args: - observable: The observable-like to format. - - Returns: - The given observable as a :const:`~BasisObservable`. - - Raises: - TypeError: If the input cannot be formatted because its type is not valid. - ValueError: If the input observable is invalid. - """ - - # Pauli-type conversions - if isinstance(observable, SparsePauliOp): - # Call simplify to combine duplicate keys before converting to a mapping - return cls.format_observable(dict(observable.simplify(atol=0).to_list())) - - if isinstance(observable, Pauli): - label, phase = observable[:].to_label(), observable.phase - return {label: 1} if phase == 0 else {label: (-1j) ** phase} - - # String conversion - if isinstance(observable, str): - cls._validate_basis(observable) - return {observable: 1} - - # Mapping conversion (with possible Pauli keys) - if isinstance(observable, MappingType): - num_qubits = len(next(iter(observable))) - unique = defaultdict(complex) - for basis, coeff in observable.items(): - if isinstance(basis, Pauli): - basis, phase = basis[:].to_label(), basis.phase - if phase != 0: - coeff = coeff * (-1j) ** phase - # Validate basis - cls._validate_basis(basis) - if len(basis) != num_qubits: - raise ValueError( - "Number of qubits must be the same for all observable basis elements." - ) - unique[basis] += coeff - return dict(unique) - - raise TypeError(f"Invalid observable type: {type(observable)}") - - @classmethod - def coerce(cls, observables: ObservablesArrayLike) -> ObservablesArray: - """Coerce ObservablesArrayLike into ObservableArray. - - Args: - observables: an object to be observables array. - - Returns: - A coerced observables array. - """ - if isinstance(observables, ObservablesArray): - return observables - if isinstance(observables, (str, SparsePauliOp, Pauli, Mapping)): - observables = [observables] - return cls(observables) - - def validate(self): - """Validate the consistency in observables array.""" - num_qubits = None - for obs in self._array.reshape(-1): - basis_num_qubits = len(next(iter(obs))) - if num_qubits is None: - num_qubits = basis_num_qubits - elif basis_num_qubits != num_qubits: - raise ValueError( - "The number of qubits must be the same for all observables in the " - "observables array." - ) - - @classmethod - def _validate_basis(cls, basis: str) -> None: - """Validate a basis string. - - Args: - basis: a basis string to validate. - - Raises: - ValueError: If basis string contains invalid characters - """ - # NOTE: the allowed basis characters can be overridden by modifying the class - # attribute ALLOWED_BASIS - allowed_pattern = _regex_match(cls.ALLOWED_BASIS) - if not allowed_pattern.match(basis): - invalid_pattern = _regex_invalid(cls.ALLOWED_BASIS) - invalid_chars = list(set(invalid_pattern.findall(basis))) - raise ValueError( - f"Observable basis string '{basis}' contains invalid characters {invalid_chars}," - f" allowed characters are {list(cls.ALLOWED_BASIS)}.", - ) - - -ObservablesArrayLike = Union[ObservablesArray, ArrayLike, BasisObservableLike] -"""Types that can be natively converted to an ObservablesArray""" - - -class PauliArray(ObservablesArray): - """An ND-array of Pauli-basis observables for an :class:`.Estimator` primitive.""" - - ALLOWED_BASIS = "IXYZ" - - -@lru_cache(1) -def _regex_match(allowed_chars: str) -> re.Pattern: - """Return pattern for matching if a string contains only the allowed characters.""" - return re.compile(f"^[{re.escape(allowed_chars)}]*$") - - -@lru_cache(1) -def _regex_invalid(allowed_chars: str) -> re.Pattern: - """Return pattern for selecting invalid strings""" - return re.compile(f"[^{re.escape(allowed_chars)}]") diff --git a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py b/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py deleted file mode 100644 index 6b90a83ed..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py +++ /dev/null @@ -1,151 +0,0 @@ -# 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. - -# Copied from qiskit, should be removed after first release -# mypy: ignore-errors -""" -Sampler Pub class -""" - -from __future__ import annotations - -from typing import Tuple, Union -from numbers import Integral - -from qiskit import QuantumCircuit - -from .bindings_array import BindingsArray, BindingsArrayLike -from .shape import ShapedMixin - - -class SamplerPub(ShapedMixin): - """Pub (Primitive Unified Bloc) for Sampler. - - Pub is composed of tuple (circuit, parameter_values, shots). - - If shots are provided this number of shots will be run with the sampler, - if ``shots=None`` the number of run shots is determined by the sampler. - """ - - def __init__( - self, - circuit: QuantumCircuit, - parameter_values: BindingsArray | None = None, - shots: int | None = None, - validate: bool = True, - ): - """Initialize a sampler pub. - - Args: - circuit: A quantum circuit. - parameter_values: A bindings array. - shots: A specific number of shots to run with. This value takes - precedence over any value owed by or supplied to a sampler. - validate: If ``True``, the input data is validated during initialization. - """ - super().__init__() - self._circuit = circuit - self._parameter_values = parameter_values or BindingsArray() - self._shots = shots - self._shape = self._parameter_values.shape - if validate: - self.validate() - - @property - def circuit(self) -> QuantumCircuit: - """A quantum circuit.""" - return self._circuit - - @property - def parameter_values(self) -> BindingsArray: - """A bindings array.""" - return self._parameter_values - - @property - def shots(self) -> int | None: - """An specific number of shots to run with (optional). - - This value takes precedence over any value owed by or supplied to a sampler. - """ - return self._shots - - @classmethod - def coerce(cls, pub: SamplerPubLike, shots: int | None = None) -> SamplerPub: - """Coerce a :class:`~.SamplerPubLike` object into a :class:`~.SamplerPub` instance. - - Args: - pub: An object to coerce. - shots: An optional default number of shots to use if not - already specified by the pub-like object. - - Returns: - A coerced sampler pub. - """ - # Validate shots kwarg if provided - if shots is not None: - if not isinstance(shots, Integral) or isinstance(shots, bool): - raise TypeError("shots must be an integer") - if shots <= 0: - raise ValueError("shots must be positive") - - if isinstance(pub, SamplerPub): - if pub.shots is None and shots is not None: - return cls( - circuit=pub.circuit, - parameter_values=pub.parameter_values, - shots=shots, - validate=False, # Assume Pub is already validated - ) - return pub - - if isinstance(pub, QuantumCircuit): - return cls(circuit=pub, shots=shots, validate=True) - - if len(pub) not in [1, 2, 3]: - raise ValueError( - f"The length of pub must be 1, 2 or 3, but length {len(pub)} is given." - ) - circuit = pub[0] - parameter_values = BindingsArray.coerce(pub[1]) if len(pub) > 1 else None - if len(pub) > 2 and pub[2] is not None: - shots = pub[2] - return cls(circuit=circuit, parameter_values=parameter_values, shots=shots, validate=True) - - def validate(self): - """Validate the pub.""" - if not isinstance(self.circuit, QuantumCircuit): - raise TypeError("circuit must be QuantumCircuit.") - - self.parameter_values.validate() - - if self.shots is not None: - if not isinstance(self.shots, Integral) or isinstance(self.shots, bool): - raise TypeError("shots must be an integer") - if self.shots <= 0: - raise ValueError("shots must be positive") - - # 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." - ) - - -SamplerPubLike = Union[ - SamplerPub, - QuantumCircuit, - Tuple[QuantumCircuit], - Tuple[QuantumCircuit, BindingsArrayLike], - Tuple[QuantumCircuit, BindingsArrayLike, Union[Integral, None]], -] diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index 52025f4f1..aa0600fcf 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -253,11 +253,20 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ # TODO proper way to do this? if isinstance(obj, EstimatorPub): return { - "circuit": obj.circuit, - "observables": obj.observables, - "parameter_values": obj.parameter_values, - "precision": obj.precision, + "__type__": "EstimatorPub", + "__value__": { + "circuit": obj.circuit, + "observables": obj.observables, + "parameter_values": obj.parameter_values, + "precision": obj.precision, + }, } + # return { + # "circuit": obj.circuit, + # "observables": obj.observables, + # "parameter_values": obj.parameter_values, + # "precision": obj.precision, + # } if isinstance(obj, ObservablesArray): return obj.tolist() if isinstance(obj, BindingsArray): diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index 85f7d47bf..4710e9693 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -17,13 +17,13 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp, Pauli, random_pauli_list +from qiskit.primitives import EstimatorPub import qiskit.quantum_info as qi import numpy as np from ddt import data, ddt from qiskit_ibm_runtime import Estimator, Session, EstimatorV2, EstimatorOptions -from qiskit_ibm_runtime.qiskit.primitives import EstimatorPub from .mock.fake_runtime_service import FakeRuntimeService from ..ibm_test_case import IBMTestCase diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index 44843d3ab..3eb4ad293 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -26,6 +26,7 @@ from qiskit.test.reference_circuits import ReferenceCircuits from qiskit.quantum_info import SparsePauliOp from qiskit.providers.fake_provider import FakeManila +from qiskit.primitives import BindingsArray from qiskit_ibm_runtime import ( Sampler, @@ -37,7 +38,7 @@ from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION from qiskit_ibm_runtime import EstimatorV2 from qiskit_ibm_runtime.estimator import Estimator as IBMBaseEstimator -from qiskit_ibm_runtime.qiskit.primitives import BindingsArray + from ..ibm_test_case import IBMTestCase from ..utils import ( diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index c48315509..683dac823 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -19,10 +19,10 @@ import numpy as np from qiskit import QuantumCircuit +from qiskit.primitives import SamplerPub from qiskit.circuit.library import RealAmplitudes from qiskit.test.reference_circuits import ReferenceCircuits from qiskit_ibm_runtime import Sampler, Session, SamplerV2, SamplerOptions -from qiskit_ibm_runtime.qiskit.primitives import SamplerPub from ..ibm_test_case import IBMTestCase from .mock.fake_runtime_service import FakeRuntimeService From 814ef5e4a3d2a12a4eec5d77d423b4cd3c8deaab Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 23 Jan 2024 14:20:19 -0500 Subject: [PATCH 11/19] lint --- .pylintrc | 1 + qiskit_ibm_runtime/utils/json.py | 17 ++++------------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index 5d5cb1eb0..15cb3e8f0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -88,6 +88,7 @@ disable=arguments-renamed, # more readable and clear too-many-public-methods, # too verbose too-many-statements, # too verbose unnecessary-pass, # allow for methods with just "pass", for clarity + no-name-in-module, # remove after qiskit 1.0 release # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index aa0600fcf..52025f4f1 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -253,20 +253,11 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ # TODO proper way to do this? if isinstance(obj, EstimatorPub): return { - "__type__": "EstimatorPub", - "__value__": { - "circuit": obj.circuit, - "observables": obj.observables, - "parameter_values": obj.parameter_values, - "precision": obj.precision, - }, + "circuit": obj.circuit, + "observables": obj.observables, + "parameter_values": obj.parameter_values, + "precision": obj.precision, } - # return { - # "circuit": obj.circuit, - # "observables": obj.observables, - # "parameter_values": obj.parameter_values, - # "precision": obj.precision, - # } if isinstance(obj, ObservablesArray): return obj.tolist() if isinstance(obj, BindingsArray): From bf65cdac55357d478668544a071565a57aa657dd Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 23 Jan 2024 15:08:48 -0500 Subject: [PATCH 12/19] improve integration test --- requirements.txt | 2 +- setup.py | 2 +- test/integration/test_estimator_v2.py | 20 ++++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0b23416ae..9ee633d5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -qiskit @ git+https://github.com/Qiskit/qiskit.git requests~=2.27 requests_ntlm>=1.1.0 numpy>=1.13 @@ -8,3 +7,4 @@ websocket-client>=1.5.1 typing-extensions>=4.0.0 ibm-platform-services>=0.22.6 qiskit-ibm-provider>=0.7.2 +qiskit @ git+https://github.com/Qiskit/qiskit.git \ No newline at end of file diff --git a/setup.py b/setup.py index ac735c079..dd9946ccb 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ import setuptools REQUIREMENTS = [ - "qiskit @ git+https://github.com/Qiskit/qiskit.git", "requests>=2.19", "requests-ntlm>=1.1.0", "numpy>=1.13", @@ -29,6 +28,7 @@ "ibm-platform-services>=0.22.6", "qiskit-ibm-provider>=0.7.2", "pydantic", + "qiskit @ git+https://github.com/Qiskit/qiskit.git", ] # Handle version. diff --git a/test/integration/test_estimator_v2.py b/test/integration/test_estimator_v2.py index a4a4198b7..7afbd6324 100644 --- a/test/integration/test_estimator_v2.py +++ b/test/integration/test_estimator_v2.py @@ -48,10 +48,22 @@ def test_estimator_v2_session(self, service): with Session(service, self.backend) as session: estimator = EstimatorV2(session=session) - job = estimator.run([(psi1, [H1, H3], [theta1, theta3]), (psi2, H2, theta2)]) + job = estimator.run([(psi1, H1, [theta1])]) result = job.result() self.assertIsInstance(result, PrimitiveResult) self.assertIsInstance(result[0], PubResult) - self.assertIsInstance(result[0].data, DataBin) - self.assertEqual(len(result[0].data.evs), 2) - self.assertEqual(len(result[0].data.stds), 2) + + job2 = estimator.run([(psi1, [H1, H3], [theta1, theta3]), (psi2, H2, theta2)]) + result2 = job2.result() + self.assertIsInstance(result2, PrimitiveResult) + self.assertIsInstance(result2[0], PubResult) + self.assertIsInstance(result2[0].data, DataBin) + self.assertEqual(len(result2[0].data.evs), 2) + self.assertEqual(len(result2[0].data.stds), 2) + + job3 = estimator.run([(psi1, H1, theta1), (psi2, H2, theta2), (psi1, H3, theta3)]) + result3 = job3.result() + self.assertIsInstance(result3, PrimitiveResult) + self.assertIsInstance(result3[2], PubResult) + self.assertIsInstance(result3[2].data, DataBin) + self.assertTrue(result3[2].metadata) From c67f61e6f8edd93a5aea3c49a3248940bed8e114 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Thu, 25 Jan 2024 11:04:51 -0500 Subject: [PATCH 13/19] remove mthree from requirements --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8387babf6..008d43fd6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,6 @@ websockets>=8 black~=22.0 coverage>=6.3 pylatexenc -mthree scikit-learn ddt>=1.2.0,!=1.4.0,!=1.4.3 From cd69d94d2c26e33f91730d6dbe54593c96fe45eb Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Wed, 7 Feb 2024 10:51:16 -0500 Subject: [PATCH 14/19] use qiskit classes --- qiskit_ibm_runtime/base_primitive.py | 3 +- qiskit_ibm_runtime/estimator.py | 91 ++++++------ .../options/environment_options.py | 3 +- .../options/estimator_options.py | 4 +- .../options/execution_options.py | 5 +- qiskit_ibm_runtime/options/options.py | 16 ++- .../options/resilience_options.py | 5 +- qiskit_ibm_runtime/options/sampler_options.py | 4 +- .../options/simulator_options.py | 5 +- .../options/transpilation_options.py | 4 +- .../options/twirling_options.py | 5 +- qiskit_ibm_runtime/options/utils.py | 8 ++ qiskit_ibm_runtime/qiskit/__init__.py | 13 -- .../qiskit/primitives/__init__.py | 15 -- .../qiskit/primitives/base_primitive.py | 53 ------- .../qiskit/primitives/base_sampler.py | 117 ---------------- .../qiskit/primitives/object_array.py | 94 ------------- .../qiskit/primitives/options.py | 40 ------ qiskit_ibm_runtime/qiskit/primitives/shape.py | 130 ------------------ qiskit_ibm_runtime/qiskit/primitives/utils.py | 58 -------- qiskit_ibm_runtime/sampler.py | 35 ++++- 21 files changed, 102 insertions(+), 606 deletions(-) delete mode 100644 qiskit_ibm_runtime/qiskit/__init__.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/__init__.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base_primitive.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/base_sampler.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/object_array.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/options.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/shape.py delete mode 100644 qiskit_ibm_runtime/qiskit/primitives/utils.py diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index adc1b2b91..b9b8e95b2 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -20,7 +20,8 @@ from dataclasses import asdict, replace import warnings -from qiskit.primitives import EstimatorPub, SamplerPub +from qiskit.primitives.containers.estimator_pub import EstimatorPub +from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.providers.options import Options as TerraOptions from qiskit_ibm_provider.session import get_cm_session as get_cm_provider_session diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 757dd5ab1..6bb425929 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -16,13 +16,14 @@ import os from typing import Optional, Dict, Sequence, Any, Union, Iterable import logging -import typing from qiskit.circuit import QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.quantum_info.operators import SparsePauliOp from qiskit.primitives import BaseEstimator from qiskit.primitives.base import BaseEstimatorV2 -from qiskit.primitives.containers import EstimatorPub, EstimatorPubLike +from qiskit.primitives.containers import EstimatorPubLike +from qiskit.primitives.containers.estimator_pub import EstimatorPub from .runtime_job import RuntimeJob from .ibm_backend import IBMBackend @@ -35,9 +36,6 @@ # pylint: disable=unused-import,cyclic-import from .session import Session -if typing.TYPE_CHECKING: - from qiskit.opflow import PauliSumOp - logger = logging.getLogger(__name__) @@ -56,41 +54,43 @@ class EstimatorV2(BasePrimitiveV2, Estimator, BaseEstimatorV2): The :meth:`run` can be used to submit circuits, observables, and parameters to the Estimator primitive. - You are encouraged to use :class:`~qiskit_ibm_runtime.Session` to open a session, - during which you can invoke one or more primitives. Jobs submitted within a session - are prioritized by the scheduler, and data is cached for efficiency. + Following construction, an estimator is used by calling its :meth:`run` method + with a list of PUBs (Primitive Unified Blocs). Each PUB contains four values that, together, + define a computation unit of work for the estimator to complete: - Example:: + * a single :class:`~qiskit.circuit.QuantumCircuit`, possibly parametrized, whose final state we + define as :math:`\psi(\theta)`, - from qiskit.circuit.library import RealAmplitudes - from qiskit.quantum_info import SparsePauliOp + * 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 - from qiskit_ibm_runtime import QiskitRuntimeService, Estimator + * a collection parameter value sets to bind the circuit against, :math:`\theta_k`. - service = QiskitRuntimeService(channel="ibm_cloud") + * an optional target precision for expectation value estimates. - psi1 = RealAmplitudes(num_qubits=2, reps=2) + Here is an example of how the estimator is used. - H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - H2 = SparsePauliOp.from_list([("IZ", 1)]) - H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) + .. code-block:: python - with Session(service=service, backend="ibmq_qasm_simulator") as session: - estimator = Estimator(session=session) + from qiskit.circuit.library import RealAmplitudes + from qiskit.quantum_info import SparsePauliOp - theta1 = [0, 1, 1, 2, 3, 5] + from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator - # calculate [ ] - psi1_H1 = estimator.run(circuits=[psi1], observables=[H1], parameter_values=[theta1]) - print(psi1_H1.result()) + service = QiskitRuntimeService() + backend = service.backend("ibmq_qasm_simulator") - # calculate [ , ] - psi1_H23 = estimator.run( - circuits=[psi1, psi1], - observables=[H2, H3], - parameter_values=[theta1]*2 - ) - print(psi1_H23.result()) + psi = RealAmplitudes(num_qubits=2, reps=2) + hamiltonian = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + theta = [0, 1, 1, 2, 3, 5] + + estimator = Estimator(backend=backend) + + # calculate [ ] + job = estimator.run([(psi, hamiltonian, [theta])]) + job_result = job.result() + print(f"The primitive-job finished with result {job_result}")) """ _options_class = EstimatorOptions @@ -131,30 +131,23 @@ def __init__( raise NotImplementedError("EstimatorV2 is not supported with q-ctrl channel strategy.") def run( - self, pubs: EstimatorPubLike | Iterable[EstimatorPubLike], *, precision: float | None = None + self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None ) -> RuntimeJob: """Submit a request to the estimator primitive. Args: - pubs: A pub-like (primitive unified bloc) object, such as a tuple, - ``(circuit, observables, parameter_values)``, or a list of pub-like objects. - precision: Target precision for expectation value estimates.. + pubs: An iterable of pub-like (primitive unified bloc) objects, such as + tuples ``(circuit, observables)`` or ``(circuit, observables, parameter_values)``. + precision: The target precision for expectation value estimates of each + run Estimator Pub that does not specify its own precision. If None + the estimator's default precision value will be used. Returns: Submitted job. """ - if isinstance(pubs, EstimatorPub): - pubs = [pubs] - elif isinstance(pubs, tuple) and isinstance(pubs[0], QuantumCircuit): - pubs = [EstimatorPub.coerce(pubs, precision=precision)] - elif pubs is not EstimatorPub: - pubs = [EstimatorPub.coerce(pub, precision=precision) for pub in pubs] - - for pub in pubs: - pub.validate() # type: ignore[union-attr] - - return self._run(pubs) # type: ignore[arg-type] + coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs] + return self._run(coerced_pubs) # type: ignore[arg-type] def _validate_options(self, options: dict) -> None: """Validate that program inputs (options) are valid @@ -262,7 +255,7 @@ def __init__( def run( # pylint: disable=arguments-differ self, circuits: QuantumCircuit | Sequence[QuantumCircuit], - observables: BaseOperator | PauliSumOp | Sequence[BaseOperator | PauliSumOp], + observables: Sequence[BaseOperator | str] | BaseOperator | str, parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, **kwargs: Any, ) -> RuntimeJob: @@ -297,9 +290,9 @@ def run( # pylint: disable=arguments-differ def _run( # pylint: disable=arguments-differ self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], - parameter_values: Sequence[Sequence[float]], + circuits: tuple[QuantumCircuit, ...], + observables: tuple[SparsePauliOp, ...], + parameter_values: tuple[tuple[float, ...], ...], **kwargs: Any, ) -> RuntimeJob: """Submit a request to the estimator primitive. diff --git a/qiskit_ibm_runtime/options/environment_options.py b/qiskit_ibm_runtime/options/environment_options.py index 9abbbbf8a..7462268c5 100644 --- a/qiskit_ibm_runtime/options/environment_options.py +++ b/qiskit_ibm_runtime/options/environment_options.py @@ -14,8 +14,7 @@ from typing import Optional, Callable, List, Literal -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import primitive_dataclass LogLevelType = Literal[ "DEBUG", diff --git a/qiskit_ibm_runtime/options/estimator_options.py b/qiskit_ibm_runtime/options/estimator_options.py index 545179185..35234bb65 100644 --- a/qiskit_ibm_runtime/options/estimator_options.py +++ b/qiskit_ibm_runtime/options/estimator_options.py @@ -27,9 +27,7 @@ from .resilience_options import ResilienceOptionsV2 from .twirling_options import TwirlingOptions from .options import OptionsV2 - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import primitive_dataclass DDSequenceType = Literal["XX", "XpXm", "XY4"] diff --git a/qiskit_ibm_runtime/options/execution_options.py b/qiskit_ibm_runtime/options/execution_options.py index fc29afb7b..8d07c9baf 100644 --- a/qiskit_ibm_runtime/options/execution_options.py +++ b/qiskit_ibm_runtime/options/execution_options.py @@ -16,10 +16,7 @@ from pydantic import model_validator, field_validator, ValidationInfo -from .utils import Unset, UnsetType, skip_unset_validation - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import Unset, UnsetType, skip_unset_validation, primitive_dataclass @primitive_dataclass diff --git a/qiskit_ibm_runtime/options/options.py b/qiskit_ibm_runtime/options/options.py index 020fdddf6..ecb591dab 100644 --- a/qiskit_ibm_runtime/options/options.py +++ b/qiskit_ibm_runtime/options/options.py @@ -21,7 +21,7 @@ from qiskit.transpiler import CouplingMap from pydantic import Field -from .utils import Dict, _to_obj, UnsetType, Unset, _remove_dict_unset_values, merge_options +from .utils import Dict, _to_obj, UnsetType, Unset, _remove_dict_unset_values, merge_options, primitive_dataclass from .environment_options import EnvironmentOptions from .execution_options import ExecutionOptionsV1 as ExecutionOptions from .simulator_options import SimulatorOptions @@ -29,9 +29,6 @@ from .resilience_options import ResilienceOptionsV1 as ResilienceOptions from ..runtime_options import RuntimeOptions -# TODO use real base options when available -from ..qiskit.primitives.options import BasePrimitiveOptions, primitive_dataclass - @dataclass class BaseOptions: @@ -66,9 +63,18 @@ def _get_runtime_options(options: dict) -> dict: return out +@dataclass +class BaseOptionsV2(BaseOptions): + """Base options for v2 primitives.""" + + def update(self, **kwargs: Any) -> None: + """Update the options.""" + for key, val in kwargs.items(): + setattr(self, key, val) + @primitive_dataclass -class OptionsV2(BaseOptions, BasePrimitiveOptions): +class OptionsV2(BaseOptionsV2): """Base primitive options, used by v2 primitives. Args: diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py index 5f6d2863d..ea265ae98 100644 --- a/qiskit_ibm_runtime/options/resilience_options.py +++ b/qiskit_ibm_runtime/options/resilience_options.py @@ -16,10 +16,7 @@ from pydantic import field_validator, model_validator -from .utils import Unset, UnsetType, skip_unset_validation - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import Unset, UnsetType, skip_unset_validation, primitive_dataclass ResilienceSupportedOptions = Literal[ diff --git a/qiskit_ibm_runtime/options/sampler_options.py b/qiskit_ibm_runtime/options/sampler_options.py index 0d0401135..a079670f1 100644 --- a/qiskit_ibm_runtime/options/sampler_options.py +++ b/qiskit_ibm_runtime/options/sampler_options.py @@ -26,9 +26,7 @@ from .transpilation_options import TranspilationOptions from .twirling_options import TwirlingOptions from .options import OptionsV2 - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import primitive_dataclass DDSequenceType = Literal["XX", "XpXm", "XY4"] diff --git a/qiskit_ibm_runtime/options/simulator_options.py b/qiskit_ibm_runtime/options/simulator_options.py index 373fbf414..b9bd479ac 100644 --- a/qiskit_ibm_runtime/options/simulator_options.py +++ b/qiskit_ibm_runtime/options/simulator_options.py @@ -21,10 +21,7 @@ from pydantic import field_validator -from .utils import Unset, UnsetType, skip_unset_validation - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import Unset, UnsetType, skip_unset_validation, primitive_dataclass class NoiseModel: diff --git a/qiskit_ibm_runtime/options/transpilation_options.py b/qiskit_ibm_runtime/options/transpilation_options.py index 7d0f3045d..10deb8fa3 100644 --- a/qiskit_ibm_runtime/options/transpilation_options.py +++ b/qiskit_ibm_runtime/options/transpilation_options.py @@ -16,10 +16,8 @@ from pydantic import field_validator -from .utils import Unset, UnsetType, skip_unset_validation +from .utils import Unset, UnsetType, skip_unset_validation, primitive_dataclass -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass LayoutMethodType = Literal[ "trivial", diff --git a/qiskit_ibm_runtime/options/twirling_options.py b/qiskit_ibm_runtime/options/twirling_options.py index 615141cbc..b7212f876 100644 --- a/qiskit_ibm_runtime/options/twirling_options.py +++ b/qiskit_ibm_runtime/options/twirling_options.py @@ -14,10 +14,7 @@ from typing import Literal, Union -from .utils import Unset, UnsetType - -# TODO use real base options when available -from ..qiskit.primitives.options import primitive_dataclass +from .utils import Unset, UnsetType, primitive_dataclass TwirlingStrategyType = Literal[ diff --git a/qiskit_ibm_runtime/options/utils.py b/qiskit_ibm_runtime/options/utils.py index a2c82a2f9..f8ef7cd18 100644 --- a/qiskit_ibm_runtime/options/utils.py +++ b/qiskit_ibm_runtime/options/utils.py @@ -17,6 +17,9 @@ import copy from dataclasses import is_dataclass, asdict +from pydantic import ConfigDict +from pydantic.dataclasses import dataclass + from ..ibm_backend import IBMBackend if TYPE_CHECKING: @@ -170,3 +173,8 @@ def __bool__(self) -> bool: Unset = UnsetType() + + +primitive_dataclass = dataclass( + config=ConfigDict(validate_assignment=True, arbitrary_types_allowed=True, extra="forbid") +) diff --git a/qiskit_ibm_runtime/qiskit/__init__.py b/qiskit_ibm_runtime/qiskit/__init__.py deleted file mode 100644 index d0229ff1b..000000000 --- a/qiskit_ibm_runtime/qiskit/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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. - -"""Temporary copy of base primitives""" diff --git a/qiskit_ibm_runtime/qiskit/primitives/__init__.py b/qiskit_ibm_runtime/qiskit/primitives/__init__.py deleted file mode 100644 index 35bf28961..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Temporary copy of base primitives""" - -from .base_sampler import BaseSamplerV2 # type: ignore[attr-defined] diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_primitive.py b/qiskit_ibm_runtime/qiskit/primitives/base_primitive.py deleted file mode 100644 index a150b9f46..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_primitive.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# type: ignore - -"""Primitive abstract base class.""" - -from __future__ import annotations -from typing import Optional - -from abc import ABC - -from .options import BasePrimitiveOptions, BasePrimitiveOptionsLike - - -class BasePrimitiveV2(ABC): - """Primitive abstract base class.""" - - version = 2 - _options_class: type[BasePrimitiveOptions] = BasePrimitiveOptions - - def __init__(self, options: Optional[BasePrimitiveOptionsLike] = None): - self._options: type(self)._options_class - self._set_options(options) - - @property - def options(self) -> BasePrimitiveOptions: - """Options for BaseEstimator""" - return self._options - - @options.setter - def options(self, options: BasePrimitiveOptionsLike) -> None: - self._set_options(options) - - def _set_options(self, options): - if options is None: - self._options = self._options_class() - elif isinstance(options, dict): - self._options = self._options_class(**options) - elif isinstance(options, self._options_class): - self._options = options - else: - raise TypeError( - f"Invalid 'options' type. It can only be a dictionary of {self._options_class}" - ) diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py b/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py deleted file mode 100644 index 74f5e225f..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py +++ /dev/null @@ -1,117 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# type: ignore - -r""" -=================== -Overview of Sampler -=================== - -Sampler class calculates probabilities or quasi-probabilities of bitstrings from quantum circuits. - -A sampler is initialized with an empty parameter set. The sampler is used to -create a :class:`~qiskit.providers.JobV1`, via the :meth:`qiskit.primitives.Sampler.run()` -method. This method is called with the following parameters - -* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits. - (a list of :class:`~qiskit.circuit.QuantumCircuit` objects) - -* parameter values (:math:`\theta_k`): list of sets of parameter values - to be bound to the parameters of the quantum circuits. - (list of list of float) - -The method returns a :class:`~qiskit.providers.JobV1` object, calling -:meth:`qiskit.providers.JobV1.result()` yields a :class:`~qiskit.primitives.SamplerResult` -object, which contains probabilities or quasi-probabilities of bitstrings, -plus optional metadata like error bars in the samples. - -Here is an example of how sampler is used. - -.. code-block:: python - - from qiskit.primitives import Sampler - from qiskit import QuantumCircuit - from qiskit.circuit.library import RealAmplitudes - - # a Bell circuit - bell = QuantumCircuit(2) - bell.h(0) - bell.cx(0, 1) - bell.measure_all() - - # two parameterized circuits - pqc = RealAmplitudes(num_qubits=2, reps=2) - pqc.measure_all() - pqc2 = RealAmplitudes(num_qubits=2, reps=3) - pqc2.measure_all() - - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 2, 3, 4, 5, 6, 7] - - # initialization of the sampler - sampler = Sampler() - - # Sampler runs a job on the Bell circuit - job = sampler.run(circuits=[bell], parameter_values=[[]], parameters=[[]]) - job_result = job.result() - print([q.binary_probabilities() for q in job_result.quasi_dists]) - - # Sampler runs a job on the parameterized circuits - job2 = sampler.run( - circuits=[pqc, pqc2], - parameter_values=[theta1, theta2], - parameters=[pqc.parameters, pqc2.parameters]) - job_result = job2.result() - print([q.binary_probabilities() for q in job_result.quasi_dists]) -""" - -from __future__ import annotations - -from abc import abstractmethod -from typing import Generic, TypeVar, Optional, Iterable - -from qiskit.circuit import QuantumCircuit -from qiskit.providers import JobV1 as Job -from qiskit.primitives.containers import SamplerPub, SamplerPubLike - -from .base_primitive import BasePrimitiveV2 -from .options import BasePrimitiveOptionsLike - -T = TypeVar("T", bound=Job) # pylint: disable=invalid-name - - -class BaseSamplerV2(BasePrimitiveV2, Generic[T]): - """Sampler base class - - Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits. - """ - - def __init__(self, options: Optional[BasePrimitiveOptionsLike] = None): - super().__init__(options=options) - - def run(self, pubs: SamplerPubLike | Iterable[SamplerPubLike]) -> T: - """TODO: docstring""" - if isinstance(pubs, SamplerPub): - pubs = [pubs] - elif isinstance(pubs, tuple) and isinstance(pubs[0], QuantumCircuit): - pubs = [SamplerPub.coerce(pubs)] - elif pubs is not SamplerPub: - pubs = [SamplerPub.coerce(pub) for pub in pubs] - - for pub in pubs: - pub.validate() - - return self._run(pubs) - - @abstractmethod - def _run(self, pubs: list[SamplerPub]) -> T: - pass diff --git a/qiskit_ibm_runtime/qiskit/primitives/object_array.py b/qiskit_ibm_runtime/qiskit/primitives/object_array.py deleted file mode 100644 index ed09717c3..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/object_array.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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. -# type: ignore - -""" -Object ND-array initialization function. -""" - -from typing import Optional, Sequence, Tuple - -import numpy as np -from numpy.typing import ArrayLike - - -def object_array( - arr: ArrayLike, - order: Optional[str] = None, - copy: bool = True, - list_types: Optional[Sequence[type]] = (), -) -> np.ndarray: - """Convert an array-like of objects into an object array. - - .. note:: - - If the objects in the array like input define ``__array__`` methods - this avoids calling them and will instead set the returned array values - to the Python objects themselves. - - Args: - arr: An array-like input. - order: Optional, the order of the returned array (C, F, A, K). If None - the default NumPy ordering of C is used. - copy: If True make a copy of the input if it is already an array. - list_types: Optional, a sequence of types to treat as lists of array - element objects when inferring the array shape from the input. - - Returns: - A NumPy ND-array with ``dtype=object``. - - Raises: - ValueError: If the input cannot be coerced into an object array. - """ - if isinstance(arr, np.ndarray): - if arr.dtype != object or order is not None or copy is True: - arr = arr.astype(object, order=order, copy=copy) - return arr - - shape = _infer_shape(arr, list_types=tuple(list_types)) - obj_arr = np.empty(shape, dtype=object, order=order) - if not shape: - # We call fill here instead of [()] to avoid invoking the - # objects `__array__` method if it has one (eg for Pauli's). - obj_arr.fill(arr) - else: - # For other arrays we need to do some tricks to avoid invoking the - # objects __array__ method by flattening the input and initializing - # using `np.fromiter` which does not invoke `__array__` for object - # dtypes. - def _flatten(nested, k): - if k == 1: - return nested - else: - return [item for sublist in nested for item in _flatten(sublist, k - 1)] - - flattened = _flatten(arr, len(shape)) - if len(flattened) != obj_arr.size: - raise ValueError( - "Input object size does not match the inferred array shape." - " This most likely occurs when the input is a ragged array." - ) - obj_arr.flat = np.fromiter(flattened, dtype=object, count=len(flattened)) - - return obj_arr - - -def _infer_shape(obj: ArrayLike, list_types: Tuple[type, ...] = ()) -> Tuple[int, ...]: - """Infer the shape of an array-like object without casting""" - if isinstance(obj, np.ndarray): - return obj.shape - if not isinstance(obj, (list, *list_types)): - return () - size = len(obj) - if size == 0: - return (size,) - return (size, *_infer_shape(obj[0], list_types=list_types)) diff --git a/qiskit_ibm_runtime/qiskit/primitives/options.py b/qiskit_ibm_runtime/qiskit/primitives/options.py deleted file mode 100644 index 5d421b81d..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/options.py +++ /dev/null @@ -1,40 +0,0 @@ -# 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. - -""" -Options class -""" - -from __future__ import annotations - -from abc import ABC -from typing import Union, Any - -from pydantic import ConfigDict -from pydantic.dataclasses import dataclass - -primitive_dataclass = dataclass( - config=ConfigDict(validate_assignment=True, arbitrary_types_allowed=True, extra="forbid") -) - - -@primitive_dataclass -class BasePrimitiveOptions(ABC): - """Base calss of options for primitives.""" - - def update(self, **kwargs: Any) -> None: - """Update the options.""" - for key, val in kwargs.items(): - setattr(self, key, val) - - -BasePrimitiveOptionsLike = Union[BasePrimitiveOptions, dict] diff --git a/qiskit_ibm_runtime/qiskit/primitives/shape.py b/qiskit_ibm_runtime/qiskit/primitives/shape.py deleted file mode 100644 index 6134ef372..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/shape.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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. -# type: ignore - -""" -Array shape related classes and functions -""" -from __future__ import annotations - -from collections.abc import Iterable -from typing import Protocol, Tuple, Union, runtime_checkable - -import numpy as np -from numpy.typing import ArrayLike, NDArray - -ShapeInput = Union[int, "Iterable[ShapeInput]"] -"""An input that is coercible into a shape tuple.""" - - -@runtime_checkable -class Shaped(Protocol): - """Protocol that defines what it means to be a shaped object. - - Note that static type checkers will classify ``numpy.ndarray`` as being :class:`Shaped`. - Moreover, since this protocol is runtime-checkable, we will even have - ``isinstance(, Shaped) == True``. - """ - - @property - def shape(self) -> Tuple[int, ...]: - """The array shape of this object.""" - raise NotImplementedError("A `Shaped` protocol must implement the `shape` property") - - @property - def ndim(self) -> int: - """The number of array dimensions of this object.""" - raise NotImplementedError("A `Shaped` protocol must implement the `ndim` property") - - @property - def size(self) -> int: - """The total dimension of this object, i.e. the product of the entries of :attr:`~shape`.""" - raise NotImplementedError("A `Shaped` protocol must implement the `size` property") - - -class ShapedMixin(Shaped): - """Mixin class to create :class:`~Shaped` types by only providing :attr:`_shape` attribute.""" - - _shape: Tuple[int, ...] - - def __repr__(self): - return f"{type(self).__name__}(<{self.shape}>)" - - @property - def shape(self): - return self._shape - - @property - def ndim(self): - return len(self._shape) - - @property - def size(self): - return int(np.prod(self._shape, dtype=int)) - - -def array_coerce(arr: Union[ArrayLike, Shaped]) -> Union[NDArray, Shaped]: - """Coerce the input into an object with a shape attribute. - - Copies are avoided. - - Args: - arr: The object to coerce. - - Returns: - Something that is :class:`~Shaped`, and always ``numpy.ndarray`` if the input is not - already :class:`~Shaped`. - """ - if isinstance(arr, Shaped): - return arr - return np.array(arr, copy=False) - - -def _flatten_to_ints(arg: ShapeInput) -> Iterable[int]: - """ - Yield one integer at a time. - - Args: - arg: Integers or iterables of integers, possibly nested, to be yielded. - - Yields: - The provided integers in depth-first recursive order. - - Raises: - ValueError: If an input is not an iterable or an integer. - """ - for item in arg: - try: - if isinstance(item, Iterable): - yield from _flatten_to_ints(item) - elif int(item) == item: - yield int(item) - else: - raise ValueError(f"Expected {item} to be iterable or an integer.") - except (TypeError, RecursionError) as ex: - raise ValueError(f"Expected {item} to be iterable or an integer.") from ex - - -def shape_tuple(*shapes: ShapeInput) -> Tuple[int, ...]: # pylint: disable=differing-param-doc - """ - Flatten the input into a single tuple of integers, preserving order. - - Args: - shapes: Integers or iterables of integers, possibly nested. - - Returns: - A tuple of integers. - - Raises: - ValueError: If some member of ``shapes`` is not an integer or iterable. - """ - return tuple(_flatten_to_ints(shapes)) diff --git a/qiskit_ibm_runtime/qiskit/primitives/utils.py b/qiskit_ibm_runtime/qiskit/primitives/utils.py deleted file mode 100644 index a8406d7cf..000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Utility functions for primitives -""" -from __future__ import annotations - -import sys - -from qiskit.circuit import ParameterExpression -from qiskit.quantum_info import SparsePauliOp -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli - - -def init_observable(observable: BaseOperator | str) -> SparsePauliOp: - """Initialize observable by converting the input to a :class:`~qiskit.quantum_info.SparsePauliOp`. - - Args: - observable: The observable. - - Returns: - The observable as :class:`~qiskit.quantum_info.SparsePauliOp`. - - Raises: - TypeError: If the observable is a :class:`~qiskit.opflow.PauliSumOp` and has a parameterized - coefficient. - """ - # This dance is to avoid importing the deprecated `qiskit.opflow` if the user hasn't already - # done so. They can't hold a `qiskit.opflow.PauliSumOp` if `qiskit.opflow` hasn't been - # imported, and we don't want unrelated Qiskit library code to be responsible for the first - # import, so the deprecation warnings will show. - if "qiskit.opflow" in sys.modules: - pauli_sum_check = sys.modules["qiskit.opflow"].PauliSumOp - else: - pauli_sum_check = () - - if isinstance(observable, SparsePauliOp): - return observable - elif isinstance(observable, pauli_sum_check): - if isinstance(observable.coeff, ParameterExpression): - raise TypeError( - f"Observable must have numerical coefficient, not {type(observable.coeff)}." - ) - return observable.coeff * observable.primitive - elif isinstance(observable, BaseOperator) and not isinstance(observable, BasePauli): - return SparsePauliOp.from_operator(observable) - else: - return SparsePauliOp(observable) diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py index 04c92c203..9b5b74e66 100644 --- a/qiskit_ibm_runtime/sampler.py +++ b/qiskit_ibm_runtime/sampler.py @@ -14,11 +14,14 @@ from __future__ import annotations import os -from typing import Dict, Optional, Sequence, Any, Union +from typing import Dict, Optional, Sequence, Any, Union, Iterable import logging +import warnings from qiskit.circuit import QuantumCircuit from qiskit.primitives import BaseSampler +from qiskit.primitives.base import BaseSamplerV2 +from qiskit.primitives.containers.sampler_pub import SamplerPub, SamplerPubLike from .options import Options from .runtime_job import RuntimeJob @@ -30,9 +33,6 @@ from .utils.qctrl import validate as qctrl_validate from .options import SamplerOptions -# TODO: remove when we have real v2 base estimator -from .qiskit.primitives import BaseSamplerV2 - logger = logging.getLogger(__name__) @@ -95,6 +95,33 @@ def __init__( # if self._service._channel_strategy == "q-ctrl": # raise NotImplementedError("SamplerV2 is not supported with q-ctrl channel strategy.") + def run( + self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None + ) -> RuntimeJob: + """Submit a request to the estimator primitive. + + Args: + pubs: An iterable of pub-like (primitive unified bloc) objects, such as + tuples ``(circuit, observables)`` or ``(circuit, observables, parameter_values)``. + precision: The target precision for expectation value estimates of each + run Estimator Pub that does not specify its own precision. If None + the estimator's default precision value will be used. + + Returns: + Submitted job. + + """ + coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs] + + if any(len(pub.circuit.cregs) == 0 for pub in coerced_pubs): + warnings.warn( + "One of your circuits has no output classical registers and so the result " + "will be empty. Did you mean to add measurement instructions?", + UserWarning, + ) + + return self._run(coerced_pubs) # type: ignore[arg-type] + def _validate_options(self, options: dict) -> None: """Validate that program inputs (options) are valid From f8f1250b1ce45cf16f6018f37e243b1cbb279d29 Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Wed, 7 Feb 2024 11:00:38 -0500 Subject: [PATCH 15/19] delete program source --- program_source/circuit_runner/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 program_source/circuit_runner/__init__.py diff --git a/program_source/circuit_runner/__init__.py b/program_source/circuit_runner/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 74e41084bb456b1e7480219e1b91d13fbfb7976e Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 7 Feb 2024 16:09:51 -0500 Subject: [PATCH 16/19] lint/typing --- qiskit_ibm_runtime/utils/json.py | 2 +- test/unit/test_data_serialization.py | 1 - .../scheduling/control_flow_test_case.py | 36 ------------------- 3 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 test/unit/transpiler/passes/scheduling/control_flow_test_case.py diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index dbd263ed1..ecb2cb731 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -216,7 +216,7 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if hasattr(obj, "to_json"): return {"__type__": "to_json", "__value__": obj.to_json()} if isinstance(obj, QuantumCircuit): - kwargs: dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)} + kwargs: Dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)} if _TERRA_VERSION[0] >= 1: # NOTE: This can be updated only after the server side has # updated to a newer qiskit version. diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index 61542e715..c6031d723 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -23,7 +23,6 @@ import numpy as np from ddt import data, ddt -import qiskit.quantum_info as qi from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate diff --git a/test/unit/transpiler/passes/scheduling/control_flow_test_case.py b/test/unit/transpiler/passes/scheduling/control_flow_test_case.py deleted file mode 100644 index b64822883..000000000 --- a/test/unit/transpiler/passes/scheduling/control_flow_test_case.py +++ /dev/null @@ -1,36 +0,0 @@ -# This code is part of Qiskit. -# -# (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 -# 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. - -"""Enhanced test case for control flow circuits.""" - -from typing import Any, Optional - -from qiskit import QuantumCircuit -from qiskit.test._canonical import canonicalize_control_flow - -from .....ibm_test_case import IBMTestCase - - -class ControlFlowTestCase(IBMTestCase): - """Test case that enforces control flow canonicalization of quantum circuits.""" - - def assertEqual( # pylint: disable=arguments-differ - self, first: Any, second: Any, msg: Optional[str] = None - ) -> None: - """Modify assertEqual to canonicalize the quantum circuit.""" - if isinstance(first, QuantumCircuit): - first = canonicalize_control_flow(first) - - if isinstance(second, QuantumCircuit): - second = canonicalize_control_flow(second) - - super().assertEqual(first, second, msg=msg) # pylint: disable=no-value-for-parameter From 37cf9a82f28aec56077891baeac95e3e90308eff Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Thu, 8 Feb 2024 11:49:19 -0500 Subject: [PATCH 17/19] Fix imports --- qiskit_ibm_runtime/base_primitive.py | 3 ++- qiskit_ibm_runtime/estimator.py | 3 ++- qiskit_ibm_runtime/qiskit/primitives/base_sampler.py | 3 ++- qiskit_ibm_runtime/utils/json.py | 4 +++- test/unit/test_data_serialization.py | 3 ++- test/unit/test_ibm_primitives_v2.py | 2 +- test/unit/test_sampler.py | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index eb11f27f6..6890d3e5e 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -20,7 +20,8 @@ from dataclasses import asdict, replace import warnings -from qiskit.primitives import EstimatorPub, SamplerPub +from qiskit.primitives.containers.estimator_pub import EstimatorPub +from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.providers.options import Options as TerraOptions from .provider_session import get_cm_session as get_cm_provider_session diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index b91fe6560..9c9887c8b 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -21,7 +21,8 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.primitives import BaseEstimator from qiskit.primitives.base import BaseEstimatorV2 -from qiskit.primitives.containers import EstimatorPub, EstimatorPubLike +from qiskit.primitives.containers import EstimatorPubLike +from qiskit.primitives.containers.estimator_pub import EstimatorPub from .runtime_job import RuntimeJob from .ibm_backend import IBMBackend diff --git a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py b/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py index 74f5e225f..556beb2a6 100644 --- a/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py +++ b/qiskit_ibm_runtime/qiskit/primitives/base_sampler.py @@ -81,7 +81,8 @@ from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job -from qiskit.primitives.containers import SamplerPub, SamplerPubLike +from qiskit.primitives.containers import SamplerPubLike +from qiskit.primitives.containers.sampler_pub import SamplerPub from .base_primitive import BasePrimitiveV2 from .options import BasePrimitiveOptionsLike diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index ecb2cb731..ee9e4a7bf 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -53,7 +53,9 @@ QuantumRegister, ) from qiskit.circuit.parametertable import ParameterView -from qiskit.primitives import ObservablesArray, BindingsArray, EstimatorPub +from qiskit.primitives.containers.observables_array import ObservablesArray +from qiskit.primitives.containers.bindings_array import BindingsArray +from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.result import Result from qiskit.version import __version__ as _terra_version_string from qiskit.utils import optionals diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index c6031d723..34f97a6a6 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -30,7 +30,8 @@ import qiskit.quantum_info as qi from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector from qiskit.result import Result -from qiskit.primitives import BindingsArray, ObservablesArray +from qiskit.primitives.containers.bindings_array import BindingsArray +from qiskit.primitives.containers.observables_array import ObservablesArray from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder from qiskit_ibm_runtime.fake_provider import FakeNairobi diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index 5434c00e3..8ffa0a007 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -25,7 +25,7 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives import BindingsArray +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit_ibm_runtime.fake_provider import FakeManila diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index 5ecee648e..b508019b5 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -19,7 +19,7 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.primitives import SamplerPub +from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.circuit.library import RealAmplitudes from qiskit_ibm_runtime import Sampler, Session, SamplerV2, SamplerOptions From e50aa423d8747dff26a91d9be8e0ce9c02f68cf2 Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Thu, 8 Feb 2024 13:56:18 -0500 Subject: [PATCH 18/19] fix bindingsarray --- qiskit_ibm_runtime/estimator.py | 8 +++---- qiskit_ibm_runtime/options/options.py | 21 ++++++++--------- qiskit_ibm_runtime/sampler.py | 14 +++++------ qiskit_ibm_runtime/utils/json.py | 34 +++++++++++++-------------- test/unit/test_data_serialization.py | 34 +++++++++++---------------- test/unit/test_estimator.py | 2 +- test/unit/test_ibm_primitives_v2.py | 8 +++---- test/unit/test_sampler.py | 2 +- 8 files changed, 55 insertions(+), 68 deletions(-) diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index ecfd6fc0c..eb1fcfd77 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -46,7 +46,7 @@ class Estimator: class EstimatorV2(BasePrimitiveV2, Estimator, BaseEstimatorV2): - """Class for interacting with Qiskit Runtime Estimator primitive service. + r"""Class for interacting with Qiskit Runtime Estimator primitive service. Qiskit Runtime Estimator primitive service estimates expectation values of quantum circuits and observables. @@ -59,11 +59,11 @@ class EstimatorV2(BasePrimitiveV2, Estimator, BaseEstimatorV2): 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)`, + 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 + :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`. diff --git a/qiskit_ibm_runtime/options/options.py b/qiskit_ibm_runtime/options/options.py index 7659fa734..0a5223e79 100644 --- a/qiskit_ibm_runtime/options/options.py +++ b/qiskit_ibm_runtime/options/options.py @@ -21,7 +21,15 @@ from qiskit.transpiler import CouplingMap from pydantic import Field -from .utils import Dict, _to_obj, UnsetType, Unset, _remove_dict_unset_values, merge_options, primitive_dataclass +from .utils import ( + Dict, + _to_obj, + UnsetType, + Unset, + _remove_dict_unset_values, + merge_options, + primitive_dataclass, +) from .environment_options import EnvironmentOptions from .execution_options import ExecutionOptionsV1 as ExecutionOptions from .simulator_options import SimulatorOptions @@ -63,18 +71,9 @@ def _get_runtime_options(options: dict) -> dict: return out -@dataclass -class BaseOptionsV2(BaseOptions): - """Base options for v2 primitives.""" - - def update(self, **kwargs: Any) -> None: - """Update the options.""" - for key, val in kwargs.items(): - setattr(self, key, val) - @primitive_dataclass -class OptionsV2(BaseOptionsV2): +class OptionsV2(BaseOptions): """Base primitive options, used by v2 primitives. Args: diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py index 9b5b74e66..2c38202e7 100644 --- a/qiskit_ibm_runtime/sampler.py +++ b/qiskit_ibm_runtime/sampler.py @@ -95,17 +95,15 @@ def __init__( # if self._service._channel_strategy == "q-ctrl": # raise NotImplementedError("SamplerV2 is not supported with q-ctrl channel strategy.") - def run( - self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None - ) -> RuntimeJob: + def run(self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None) -> RuntimeJob: """Submit a request to the estimator primitive. Args: - pubs: An iterable of pub-like (primitive unified bloc) objects, such as - tuples ``(circuit, observables)`` or ``(circuit, observables, parameter_values)``. - precision: The target precision for expectation value estimates of each - run Estimator Pub that does not specify its own precision. If None - the estimator's default precision value will be used. + pubs: An iterable of pub-like objects. For example, a list of circuits + or tuples ``(circuit, parameter_values)``. + shots: The total number of shots to sample for each sampler pub that does + not specify its own shots. If ``None``, the primitive's default + shots value will be used, which can vary by implementation. Returns: Submitted job. diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index dbd263ed1..1098b6d55 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -53,7 +53,9 @@ QuantumRegister, ) from qiskit.circuit.parametertable import ParameterView -from qiskit.primitives import ObservablesArray, BindingsArray, EstimatorPub +from qiskit.primitives.containers.estimator_pub import EstimatorPub +from qiskit.primitives.containers.observables_array import ObservablesArray +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit.result import Result from qiskit.version import __version__ as _terra_version_string from qiskit.utils import optionals @@ -273,15 +275,11 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if isinstance(obj, ObservablesArray): return obj.tolist() if isinstance(obj, BindingsArray): - out_val = {} - if obj.kwvals: - encoded_kwvals = {} - for key, val in obj.kwvals.items(): - encoded_kwvals[json.dumps(key, cls=RuntimeEncoder)] = val - out_val["kwvals"] = encoded_kwvals - if obj.vals: - out_val["vals"] = obj.vals # type: ignore[assignment] - out_val["shape"] = obj.shape + out_val = {"shape": obj.shape} + encoded_data = {} + for key, val in obj.data.items(): + encoded_data[json.dumps(key, cls=RuntimeEncoder)] = val + out_val["data"] = encoded_data return {"__type__": "BindingsArray", "__value__": out_val} if HAS_AER and isinstance(obj, qiskit_aer.noise.NoiseModel): @@ -358,16 +356,16 @@ def object_hook(self, obj: Any) -> Any: return _decode_and_deserialize(obj_val, scipy.sparse.load_npz, False) if obj_type == "BindingsArray": ba_kwargs = {"shape": obj_val.get("shape", None)} - kwvals = obj_val.get("kwvals", None) - if isinstance(kwvals, dict): - kwvals_decoded = {} - for key, val in kwvals.items(): + data = obj_val.get("data", None) + if isinstance(data, dict): + data_decoded = {} + for key, val in data.items(): # Convert to tuple or it can't be a key decoded_key = tuple(json.loads(key, cls=RuntimeDecoder)) - kwvals_decoded[decoded_key] = val - ba_kwargs["kwvals"] = kwvals_decoded - elif kwvals: - raise ValueError(f"Unexpected kwvals type {type(kwvals)} in BindingsArray.") + data_decoded[decoded_key] = val + ba_kwargs["data"] = data_decoded + elif data: + raise ValueError(f"Unexpected data type {type(data)} in BindingsArray.") ba_kwargs["vals"] = obj_val.get("vals", None) return BindingsArray(**ba_kwargs) diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index bff679161..28aa27f48 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -23,14 +23,13 @@ import numpy as np from ddt import data, ddt -import qiskit.quantum_info as qi from qiskit.circuit import Parameter, QuantumCircuit - from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate import qiskit.quantum_info as qi from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector from qiskit.result import Result -from qiskit.primitives import BindingsArray, ObservablesArray +from qiskit.primitives.containers.observables_array import ObservablesArray +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder from qiskit_ibm_runtime.fake_provider import FakeNairobi @@ -282,26 +281,21 @@ def test_obs_array(self, oarray): self.assertEqual(decoded, oarray.tolist()) @data( - BindingsArray([1, 2, 3.4]), - BindingsArray([4.0, 5.0, 6.0], shape=()), - BindingsArray([[1 + 2j, 2 + 3j], [3 + 4j, 4 + 5j]], shape=(2,)), - BindingsArray(np.random.uniform(size=(5,))), - BindingsArray(np.linspace(0, 1, 30).reshape((2, 3, 5))), - BindingsArray(kwvals={Parameter("a"): [0.0], Parameter("b"): [1.0]}, shape=1), + BindingsArray(data={Parameter("a"): [0.0], Parameter("b"): [1.0]}, shape=1), BindingsArray( - kwvals={ + data={ (Parameter("a"), Parameter("b")): np.random.random((4, 3, 2)), Parameter("c"): np.random.random((4, 3)), } ), BindingsArray( - vals=np.random.random((2, 3, 4)), - kwvals={ + data={ (Parameter("a"), Parameter("b")): np.random.random((2, 3, 2)), Parameter("c"): np.random.random((2, 3)), }, ), - BindingsArray(vals=[[1.0, 2.0], [1.1, 2.1]], kwvals={Parameter("c"): [3.0, 3.1]}), + BindingsArray(data={Parameter("c"): [3.0, 3.1]}), + BindingsArray(data={"param1": [1, 2, 3], "param2": [3, 4, 5]}), ) def test_bindings_array(self, barray): """Test encoding and decoding BindingsArray.""" @@ -319,10 +313,10 @@ def _to_str_keyed(_in_dict): decoded = json.loads(encoded, cls=RuntimeDecoder)["array"] self.assertIsInstance(decoded, BindingsArray) self.assertEqual(barray.shape, decoded.shape) - self.assertTrue(np.allclose(barray.vals, decoded.vals)) - if barray.kwvals: - barray_str_keyed = _to_str_keyed(barray.kwvals) - decoded_str_keyed = _to_str_keyed(decoded.kwvals) - for key, val in barray_str_keyed.items(): - self.assertIn(key, decoded_str_keyed) - self.assertTrue(np.allclose(val, decoded_str_keyed[key])) + self.assertTrue(np.allclose(barray.data, decoded.data)) + + barray_str_keyed = _to_str_keyed(barray.data) + decoded_str_keyed = _to_str_keyed(decoded.data) + for key, val in barray_str_keyed.items(): + self.assertIn(key, decoded_str_keyed) + self.assertTrue(np.allclose(val, decoded_str_keyed[key])) diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index 3898a8e1d..483dbc737 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -17,7 +17,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp, Pauli, random_pauli_list -from qiskit.primitives import EstimatorPub +from qiskit.primitives.containers.estimator_pub import EstimatorPub import qiskit.quantum_info as qi import numpy as np diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index 8452f8677..7d804f407 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -24,7 +24,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives import BindingsArray +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit_ibm_runtime import ( Sampler, @@ -309,14 +309,12 @@ def test_parameters_vals_kwvals(self, primitive): with self.subTest("0-d"): param_vals = np.linspace(0, 1, 4) - kwvals = {tuple(circ.parameters[:2]): param_vals[:2]} - barray = BindingsArray(vals=param_vals[2:], kwvals=kwvals) + barray = BindingsArray(data={tuple(circ.parameters): param_vals}) pub = (circ, "ZZ", barray) if isinstance(inst, EstimatorV2) else (circ, barray) inst.run(pub) with self.subTest("n-d"): - kwvals = {tuple(circ.parameters[:2]): np.random.random((2, 3, 2))} - barray = BindingsArray(vals=np.random.random((2, 3, 2)), kwvals=kwvals) + barray = BindingsArray(data={tuple(circ.parameters): np.random.random((2, 3, 4))}) pub = (circ, "ZZ", barray) if isinstance(inst, EstimatorV2) else (circ, barray) inst.run(pub) diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index 5ecee648e..b508019b5 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -19,7 +19,7 @@ import numpy as np from qiskit import QuantumCircuit -from qiskit.primitives import SamplerPub +from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.circuit.library import RealAmplitudes from qiskit_ibm_runtime import Sampler, Session, SamplerV2, SamplerOptions From c36bcacad6765416c403c23a562930dba62062da Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Thu, 8 Feb 2024 15:29:53 -0500 Subject: [PATCH 19/19] fix tests --- qiskit_ibm_runtime/utils/json.py | 1 - test/unit/test_data_serialization.py | 8 +++-- test/unit/test_estimator.py | 49 +++++++++++++--------------- test/unit/test_ibm_primitives_v2.py | 23 ++++++------- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index f50eddafe..ea980ae69 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -366,7 +366,6 @@ def object_hook(self, obj: Any) -> Any: ba_kwargs["data"] = data_decoded elif data: raise ValueError(f"Unexpected data type {type(data)} in BindingsArray.") - ba_kwargs["vals"] = obj_val.get("vals", None) return BindingsArray(**ba_kwargs) diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index 817ee70e5..840bb72ea 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -267,10 +267,13 @@ def test_encoder_pubs(self): @data( ObservablesArray([["X", "Y", "Z"], ["0", "1", "+"]]), ObservablesArray(qi.pauli_basis(2)), - ObservablesArray([qi.random_pauli_list(2, 3) for _ in range(5)]), + ObservablesArray([qi.random_pauli_list(2, 3, phase=False) for _ in range(5)]), ObservablesArray(np.array([["X", "Y"], ["Z", "I"]], dtype=object)), ObservablesArray( - [[SparsePauliOp(qi.random_pauli_list(2, 3)) for _ in range(3)] for _ in range(5)] + [ + [SparsePauliOp(qi.random_pauli_list(2, 3, phase=False)) for _ in range(3)] + for _ in range(5) + ] ), ) def test_obs_array(self, oarray): @@ -314,7 +317,6 @@ def _to_str_keyed(_in_dict): decoded = json.loads(encoded, cls=RuntimeDecoder)["array"] self.assertIsInstance(decoded, BindingsArray) self.assertEqual(barray.shape, decoded.shape) - self.assertTrue(np.allclose(barray.data, decoded.data)) barray_str_keyed = _to_str_keyed(barray.data) decoded_str_keyed = _to_str_keyed(decoded.data) diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index 483dbc737..4444fa66a 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -18,7 +18,6 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp, Pauli, random_pauli_list from qiskit.primitives.containers.estimator_pub import EstimatorPub -import qiskit.quantum_info as qi import numpy as np from ddt import data, ddt @@ -66,10 +65,10 @@ def setUp(self) -> None: self.observables = SparsePauliOp.from_list([("I", 1)]) @data( - [(RealAmplitudes(num_qubits=2, reps=1), ["ZZ"], [1, 2, 3, 4])], - [(RealAmplitudes(num_qubits=2, reps=1), ["ZZ", "YY"], [1, 2, 3, 4])], + [(RealAmplitudes(num_qubits=2, reps=1), ["ZZ"], [[1, 2, 3, 4]])], + [(RealAmplitudes(num_qubits=2, reps=1), ["ZZ", "YY"], [[1, 2, 3, 4]])], [(QuantumCircuit(2), ["XX"])], - [(RealAmplitudes(num_qubits=1, reps=1), ["I"], [1, 2]), (QuantumCircuit(3), ["YYY"])], + [(RealAmplitudes(num_qubits=1, reps=1), ["I"], [[1, 2]]), (QuantumCircuit(3), ["YYY"])], ) def test_run_program_inputs(self, in_pubs): """Verify program inputs are correct.""" @@ -89,7 +88,8 @@ def test_run_program_inputs(self, in_pubs): self.assertEqual(list(a_pub_obs.keys())[0], an_input_obs) # Check parameter values an_input_params = an_in_taks[2] if len(an_in_taks) == 3 else [] - np.allclose(a_pub_param.parameter_values.vals, an_input_params) + a_pub_param_values = list(a_pub_param.parameter_values.data.values()) + np.allclose(a_pub_param_values, an_input_params) def test_unsupported_values_for_estimator_options(self): """Test exception when options levels are not supported.""" @@ -116,7 +116,7 @@ def test_pec_simulator(self): inst = EstimatorV2(session=session, options={"resilience": {"pec_mitigation": True}}) with self.assertRaises(ValueError) as exc: - inst.run((self.circuit, self.observables)) + inst.run([(self.circuit, self.observables)]) self.assertIn("coupling map", str(exc.exception)) def test_run_default_options(self): @@ -141,7 +141,7 @@ def test_run_default_options(self): for options, expected in options_vars: with self.subTest(options=options): inst = EstimatorV2(session=session, options=options) - inst.run((self.circuit, self.observables)) + inst.run([(self.circuit, self.observables)]) inputs = session.run.call_args.kwargs["inputs"] self.assertTrue( dict_paritally_equal(inputs, expected), @@ -162,8 +162,8 @@ def test_invalid_resilience_options(self, res_opt): if len(res_opt.keys()) > 1: self.assertIn(list(res_opt.keys())[1], str(exc.exception)) - @data(True, False) - def test_observable_types_single_circuit(self, to_pub): + # ObservablesArray([qi.random_pauli_list(2, 3, phase=False) for _ in range(5)]), + def test_observable_types_single_circuit(self): """Test different observable types for a single circuit.""" all_obs = [ # TODO: Uncomment single ObservableArrayLike when supported @@ -173,19 +173,18 @@ def test_observable_types_single_circuit(self, to_pub): # {"YZ": 1 + 2j}, # {Pauli("XX"): 1 + 2j}, ["XX", "YY"], - [qi.random_pauli_list(2)], + [random_pauli_list(2, 3, phase=False)], [Pauli("XX"), Pauli("YY")], - [SparsePauliOp(["XX"]), SparsePauliOp(["YY"])], + [SparsePauliOp(["XX"], [2]), SparsePauliOp(["YY"], [1])], [ - {"XX": 1 + 2j}, - {"YY": 1 + 2j}, + {"XX": 1}, + {"YY": 2}, ], [ - {Pauli("XX"): 1 + 2j}, - {Pauli("YY"): 1 + 2j}, + {Pauli("XX"): 1}, + {Pauli("YY"): 2}, ], - [random_pauli_list(2, 2)], - [random_pauli_list(2, 3) for _ in range(5)], + [random_pauli_list(2, 3, phase=False) for _ in range(5)], np.array([["II", "XX", "YY"], ["ZZ", "XZ", "II"]], dtype=object), ] @@ -194,9 +193,7 @@ def test_observable_types_single_circuit(self, to_pub): for obs in all_obs: with self.subTest(obs=obs): pub = (circuit, obs) - if to_pub: - pub = EstimatorPub.coerce(pub) - estimator.run(pub) + estimator.run([pub]) def test_observable_types_multi_circuits(self): """Test different observable types for multiple circuits.""" @@ -216,15 +213,15 @@ def test_observable_types_multi_circuits(self): [["XX", "YY"], ["ZZZ", "III"]], [[Pauli("XX"), Pauli("YY")], [Pauli("XXX"), Pauli("YYY")]], [ - [SparsePauliOp(["XX"]), SparsePauliOp(["YY"])], - [SparsePauliOp(["XXX"]), SparsePauliOp(["YYY"])], + [SparsePauliOp(["XX", "YY"], [1, 2]), SparsePauliOp(["YY", "-XX"], [2, 1])], + [SparsePauliOp(["XXX"], [1]), SparsePauliOp(["YYY"], [2])], ], - [[{"XX": 1 + 2j}, {"YY": 1 + 2j}], [{"XXX": 1 + 2j}, {"YYY": 1 + 2j}]], + [[{"XX": 1}, {"YY": 2}], [{"XXX": 1}, {"YYY": 2}]], [ - [{Pauli("XX"): 1 + 2j}, {Pauli("YY"): 1 + 2j}], - [{Pauli("XXX"): 1 + 2j}, {Pauli("YYY"): 1 + 2j}], + [{Pauli("XX"): 1}, {Pauli("YY"): 2}], + [{Pauli("XXX"): 1}, {Pauli("YYY"): 2}], ], - [random_pauli_list(2, 2), random_pauli_list(3, 2)], + [random_pauli_list(2, 2, phase=False), random_pauli_list(3, 2, phase=False)], ] circuit1 = QuantumCircuit(2) diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index 3d9411c8a..36f693386 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -25,9 +25,6 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives.containers.bindings_array import BindingsArray - - from qiskit_ibm_runtime import ( Sampler, Estimator, @@ -301,7 +298,7 @@ def test_parameters_single_circuit(self, primitive): for val in param_vals: with self.subTest(val=val): pub = (circ, "ZZ", val) if isinstance(inst, EstimatorV2) else (circ, val) - inst.run(pub) + inst.run([pub]) @data(EstimatorV2) def test_parameters_vals_kwvals(self, primitive): @@ -311,14 +308,14 @@ def test_parameters_vals_kwvals(self, primitive): with self.subTest("0-d"): param_vals = np.linspace(0, 1, 4) - barray = BindingsArray(data={tuple(circ.parameters): param_vals}) + barray = {tuple(circ.parameters): param_vals} pub = (circ, "ZZ", barray) if isinstance(inst, EstimatorV2) else (circ, barray) - inst.run(pub) + inst.run([pub]) with self.subTest("n-d"): - barray = BindingsArray(data={tuple(circ.parameters): np.random.random((2, 3, 4))}) + barray = {tuple(circ.parameters): np.random.random((2, 3, 4))} pub = (circ, "ZZ", barray) if isinstance(inst, EstimatorV2) else (circ, barray) - inst.run(pub) + inst.run([pub]) @data(EstimatorV2) def test_parameters_multiple_circuits(self, primitive): @@ -590,7 +587,7 @@ def test_raise_faulty_qubits(self, primitive): pub = (transpiled,) with self.assertRaises(ValueError) as err: - inst.run(pubs=pub) + inst.run(pubs=[pub]) self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception)) @data(EstimatorV2) @@ -650,7 +647,7 @@ def test_raise_faulty_edge(self, primitive): pub = (transpiled,) with self.assertRaises(ValueError) as err: - inst.run(pubs=pub) + inst.run(pubs=[pub]) self.assertIn("cx", str(err.exception)) self.assertIn(f"faulty edge {tuple(edge_qubits)}", str(err.exception)) @@ -679,7 +676,7 @@ def test_faulty_qubit_not_used(self, primitive): pub = (transpiled,) with patch.object(Session, "run") as mock_run: - inst.run(pub) + inst.run([pub]) mock_run.assert_called_once() @data(EstimatorV2) @@ -709,7 +706,7 @@ def test_faulty_edge_not_used(self, primitive): pub = (transpiled,) with patch.object(Session, "run") as mock_run: - inst.run(pub) + inst.run([pub]) mock_run.assert_called_once() @data(EstimatorV2) @@ -740,7 +737,7 @@ def test_no_raise_skip_transpilation(self, primitive): pub = (transpiled,) with patch.object(Session, "run") as mock_run: - inst.run(pub) + inst.run([pub]) mock_run.assert_called_once() def _update_dict(self, dict1, dict2):