diff --git a/.pylintrc b/.pylintrc index a455fd605e..c13d270f9d 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/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index effaa68979..6890d3e5e7 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -20,6 +20,8 @@ from dataclasses import asdict, replace import warnings +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 @@ -33,8 +35,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 @@ -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)) @@ -170,13 +170,18 @@ 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: 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 ba7e56f4d3..eb1fcfd77e 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -14,12 +14,16 @@ 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 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 EstimatorPubLike +from qiskit.primitives.containers.estimator_pub import EstimatorPub from .runtime_job import RuntimeJob from .ibm_backend import IBMBackend @@ -28,8 +32,6 @@ from .base_primitive import BasePrimitiveV1, BasePrimitiveV2 from .utils.qctrl import validate as qctrl_validate -# TODO: remove when we have real v2 base estimator -from .qiskit.primitives import BaseEstimatorV2 # pylint: disable=unused-import,cyclic-import from .session import Session @@ -44,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. @@ -52,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 @@ -126,6 +130,25 @@ def __init__( if self._service._channel_strategy == "q-ctrl": raise NotImplementedError("EstimatorV2 is not supported with q-ctrl channel strategy.") + def run( + self, pubs: Iterable[EstimatorPubLike], *, precision: float | 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 = [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 @@ -232,7 +255,7 @@ def __init__( def run( # pylint: disable=arguments-differ self, circuits: QuantumCircuit | Sequence[QuantumCircuit], - observables: BaseOperator | Sequence[BaseOperator], + observables: Sequence[BaseOperator | str] | BaseOperator | str, parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, **kwargs: Any, ) -> RuntimeJob: @@ -267,9 +290,9 @@ def run( # pylint: disable=arguments-differ def _run( # pylint: disable=arguments-differ self, - circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator], - 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 9abbbbf8a4..7462268c53 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 0a5733c08b..55a01966cf 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"] MAX_RESILIENCE_LEVEL: int = 2 diff --git a/qiskit_ibm_runtime/options/execution_options.py b/qiskit_ibm_runtime/options/execution_options.py index fc29afb7b9..8d07c9baf1 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 c894e2a071..0a5223e797 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 +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 +37,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: @@ -68,7 +73,7 @@ def _get_runtime_options(options: dict) -> dict: @primitive_dataclass -class OptionsV2(BaseOptions, BasePrimitiveOptions): +class OptionsV2(BaseOptions): """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 5f6d2863d5..ea265ae98b 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 debdb6443b..f4b2fec3f8 100644 --- a/qiskit_ibm_runtime/options/sampler_options.py +++ b/qiskit_ibm_runtime/options/sampler_options.py @@ -25,9 +25,7 @@ from .transpilation_options import TranspilationOptionsV2 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 373fbf4147..b9bd479ac0 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 54d09a791b..610527a2db 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 615141cbc3..b7212f8762 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 a2c82a2f9a..f8ef7cd18d 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 d0229ff1b2..0000000000 --- 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 2ca4c26e6d..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/__init__.py +++ /dev/null @@ -1,20 +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_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] -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_estimator.py b/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py deleted file mode 100644 index 3da9630e07..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_estimator.py +++ /dev/null @@ -1,119 +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 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. - -.. math:: - - \langle\psi_i(\theta_k)|H_j|\psi_i(\theta_k)\rangle - -Here is an example of how the estimator is used. - -.. code-block:: python - - from qiskit.primitives 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], [H1], [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]) - 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 Generic, TypeVar, Iterable, Optional - -from qiskit.circuit import QuantumCircuit -from qiskit.providers import JobV1 as Job - -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() - - return self._run(pubs) - - @abstractmethod - def _run(self, pubs: list[EstimatorPub]) -> T: - pass 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 a150b9f463..0000000000 --- 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_pub.py b/qiskit_ibm_runtime/qiskit/primitives/base_pub.py deleted file mode 100644 index da2d31d952..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/base_pub.py +++ /dev/null @@ -1,37 +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 dataclasses import dataclass - -from qiskit import QuantumCircuit - - -@dataclass(frozen=True) -class BasePub: - """Base class for Pub""" - - circuit: QuantumCircuit - - def validate(self) -> None: - """Validate the inputs. - - Raises: - TypeError: If input values has an invalid type. - """ - if not isinstance(self.circuit, QuantumCircuit): - raise TypeError("circuit must be QuantumCircuit.") 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 7440c3f18c..0000000000 --- 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 .base_primitive import BasePrimitiveV2 -from .options import BasePrimitiveOptionsLike -from .sampler_pub import SamplerPub, SamplerPubLike - -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/bindings_array.py b/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py deleted file mode 100644 index 4aa5096b80..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/bindings_array.py +++ /dev/null @@ -1,455 +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 - -""" -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 - -import numpy as np -from numpy.typing import ArrayLike, NDArray - -from qiskit.circuit import Parameter, QuantumCircuit - -from .shape import ShapedMixin, ShapeInput, shape_tuple - - -class BindingsArray(ShapedMixin): - r"""Stores many possible parameter binding values 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``. - - 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. - - .. 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))} - ) - """ - - def __init__( - self, - vals: Union[None, ArrayLike, Iterable[ArrayLike]] = None, - kwvals: Union[None, Mapping[Parameter, Iterable[Parameter]], ArrayLike] = None, - shape: Optional[ShapeInput] = None, - ): - """ - 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 = { - (p,) if isinstance(p, Parameter) else tuple(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) - 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._kwvals = kwvals - - 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) - - @property - def kwvals(self) -> Dict[Tuple[Parameter, ...], 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: Optional[Iterable[Parameter]] = 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 it is up to the user to ensure that their provided ``parameters`` account for this. - - 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 multple dtypes. - KeyError: If ``parameters`` are provided that are not a superset of those in this - bindings array. - """ - 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 - - 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(): - size = arr.shape[-1] - ret[..., pos : pos + size] = arr - pos += size - elif self.kwvals: - # use the order of the provided parameters - parameters = {_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] - except KeyError as ex: - raise KeyError( - "This bindings array has a parameter absent from the provided parameters." - ) 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. - - Args: - circuit: The circuit to bind. - idx: 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(idx) != self.ndim: - raise ValueError(f"Expected {idx} to index all dimensions of {self.shape}") - - flat_vals = (val for vals in self.vals for val in vals[idx]) - - 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]) - ) - 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. - - 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_at_idx(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: ShapeInput) -> 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_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) - - @classmethod - def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray: - """Coerce BindingsArrayLike into BindingsArray - - Args: - bindings_array: an object to be bindings array. - - Returns: - A coerced bindings array. - - Raises: - TypeError: If input value type is invalid. - """ - 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,)) - 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." - ) - 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." - ) - - -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") - - -BindingsArrayLike = Union[ - BindingsArray, - NDArray, - Mapping[Parameter, NDArray], - Sequence[NDArray], - 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 3fa517fb7b..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/estimator_pub.py +++ /dev/null @@ -1,97 +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 - -""" -Estiamtor Pub class -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Union, Tuple - -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). - """ - - observables: ObservablesArray - parameter_values: BindingsArray = BindingsArray([], shape=()) - _shape: tuple[int, ...] = field(init=False) - - def __post_init__(self): - shape = np.broadcast_shapes(self.observables.shape, self.parameter_values.shape) - super().__setattr__("_shape", shape) - - @classmethod - def coerce(cls, pub: EstimatorPubLike) -> EstimatorPub: - """Coerce EstimatorPubLike into EstimatorPub. - - Args: - pub: an object to be estimator pub. - - Returns: - A coerced estiamtor pub. - - Raises: - ValueError: If input values are invalid. - """ - if isinstance(pub, EstimatorPub): - return pub - if len(pub) != 2 and len(pub) != 3: - raise ValueError(f"The length of pub must be 2 or 3, but length {len(pub)} is given.") - circuit = pub[0] - observables = ObservablesArray.coerce(pub[1]) - parameter_values = ( - BindingsArray.coerce(pub[2]) if len(pub) == 3 else BindingsArray([], shape=(1,)) - ) - return cls(circuit=circuit, observables=observables, parameter_values=parameter_values) - - def validate(self) -> None: - """Validate the pub.""" - super().validate() - self.observables.validate() - self.parameter_values.validate() - # Cross validate circuits and observables - # for i, observable in enumerate(self.observables): - # num_qubits = len(next(iter(observable))) - - 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 - 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, BindingsArrayLike] -] 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 ed09717c3f..0000000000 --- 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/observables_array.py b/qiskit_ibm_runtime/qiskit/primitives/observables_array.py deleted file mode 100644 index 07c36fc639..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/observables_array.py +++ /dev/null @@ -1,247 +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 - -""" -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 - -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.""" - - ALLOWED_BASIS: str = "IXYZ01+-lr" - """The allowed characters in :const:`BasisObservable` strings.""" - - def __init__( - self, - observables: Union[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'") - - def __getitem__(self, args) -> Union[ObservablesArray, BasisObservable]: - 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": - """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.""" - pass - - @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/options.py b/qiskit_ibm_runtime/qiskit/primitives/options.py deleted file mode 100644 index 5d421b81d3..0000000000 --- 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/pub_result.py b/qiskit_ibm_runtime/qiskit/primitives/pub_result.py deleted file mode 100644 index 467bfb9b3e..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/pub_result.py +++ /dev/null @@ -1,27 +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 dataclasses import dataclass - - -@dataclass(frozen=True) -class PubsResult: - """Result of pub.""" - - data: dict - metadata: dict 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 f82d4ed3dc..0000000000 --- a/qiskit_ibm_runtime/qiskit/primitives/sampler_pub.py +++ /dev/null @@ -1,82 +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 - -""" -Sampler PUB class -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Union, Optional, Tuple - -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). - """ - - parameter_values: Optional[BindingsArray] = BindingsArray([], shape=()) - _shape: tuple[int, ...] = field(init=False) - - def __post_init__(self): - super().__setattr__("_shape", self.parameter_values.shape) - - @classmethod - def coerce(cls, pub: SamplerPubLike) -> SamplerPub: - """Coerce SamplerPubLike into SamplerPub. - - Args: - pub: an object to be sampler pub. - - Returns: - A coerced estiamtor pub. - - Raises: - ValueError: If input values are invalid. - """ - if isinstance(pub, SamplerPub): - 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.") - circuit = pub[0] - parameter_values = ( - BindingsArray.coerce(pub[1]) if len(pub) == 2 else BindingsArray([], shape=()) - ) - return cls(circuit=circuit, parameter_values=parameter_values) - - def validate(self) -> None: - """Validate the pub. - - Raises: - ValueError: If input values are invalid. - """ - super().validate() - self.parameter_values.validate() - # Cross validate circuits and paramter_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, Tuple[QuantumCircuit, BindingsArrayLike]] diff --git a/qiskit_ibm_runtime/qiskit/primitives/shape.py b/qiskit_ibm_runtime/qiskit/primitives/shape.py deleted file mode 100644 index 6134ef3722..0000000000 --- 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 a8406d7cf9..0000000000 --- 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 04c92c2031..2c38202e7f 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,31 @@ 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 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. + + """ + 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 diff --git a/qiskit_ibm_runtime/utils/estimator_result_decoder.py b/qiskit_ibm_runtime/utils/estimator_result_decoder.py index 3d35836817..513ec984b9 100644 --- a/qiskit_ibm_runtime/utils/estimator_result_decoder.py +++ b/qiskit_ibm_runtime/utils/estimator_result_decoder.py @@ -16,9 +16,9 @@ import numpy as np from qiskit.primitives import EstimatorResult +from qiskit.primitives.containers import PrimitiveResult, make_data_bin, PubResult from .result_decoder import ResultDecoder -from ..qiskit.primitives.pub_result import PubsResult class EstimatorResultDecoder(ResultDecoder): @@ -35,10 +35,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( - PubsResult(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"], diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index eca5069f5a..ea980ae691 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 @@ -54,6 +53,9 @@ QuantumRegister, ) from qiskit.circuit.parametertable import ParameterView +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 @@ -66,10 +68,6 @@ ) from qiskit.qpy.binary_io.value import _write_parameter, _read_parameter -# 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] ) @@ -266,20 +264,22 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ ), # 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): - 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): @@ -356,17 +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.") - ba_kwargs["vals"] = obj_val.get("vals", None) + data_decoded[decoded_key] = val + ba_kwargs["data"] = data_decoded + elif data: + raise ValueError(f"Unexpected data type {type(data)} in BindingsArray.") return BindingsArray(**ba_kwargs) diff --git a/requirements.txt b/requirements.txt index 4f82de5692..ae64094a7c 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 @@ -7,3 +6,4 @@ python-dateutil>=2.8.0 websocket-client>=1.5.1 typing-extensions>=4.0.0 ibm-platform-services>=0.22.6 +qiskit @ git+https://github.com/Qiskit/qiskit.git \ No newline at end of file diff --git a/setup.py b/setup.py index 64a1057a03..b10321b20c 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", @@ -28,6 +27,7 @@ "websocket-client>=1.5.1", "ibm-platform-services>=0.22.6", "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 new file mode 100644 index 0000000000..7afbd63244 --- /dev/null +++ b/test/integration/test_estimator_v2.py @@ -0,0 +1,69 @@ +# 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 + +from qiskit.primitives.containers import PrimitiveResult, PubResult, 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, [theta1])]) + result = job.result() + self.assertIsInstance(result, PrimitiveResult) + self.assertIsInstance(result[0], PubResult) + + 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) diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index ca6fcc19fa..840bb72eaf 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -24,17 +24,17 @@ from ddt import data, ddt 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.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 -# 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 @@ -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): @@ -282,26 +285,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.""" @@ -309,7 +307,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 @@ -318,10 +317,9 @@ 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])) + + 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 6ea645246f..4444fa66a4 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -17,13 +17,12 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp, Pauli, random_pauli_list -import qiskit.quantum_info as qi +from qiskit.primitives.containers.estimator_pub import EstimatorPub 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 @@ -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.""" @@ -77,8 +76,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 @@ -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 438ad5f06c..36f6933869 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -24,7 +24,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import ( Sampler, @@ -36,7 +35,8 @@ 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 qiskit_ibm_runtime.fake_provider import FakeManila + from ..ibm_test_case import IBMTestCase from ..utils import ( @@ -298,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): @@ -308,16 +308,14 @@ 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 = {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"): - kwvals = {tuple(circ.parameters[:2]): np.random.random((2, 3, 2))} - barray = BindingsArray(vals=np.random.random((2, 3, 2)), kwvals=kwvals) + 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): @@ -417,7 +415,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"]: + for fld in ["pubs"]: inputs.pop(fld, None) expected = {"skip_transpilation": False, "execution": {"init_qubits": True}, "version": 2} self.assertDictEqual(inputs, expected) @@ -589,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) @@ -649,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)) @@ -678,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) @@ -708,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) @@ -739,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): diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index f2e694a713..b508019b51 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -19,9 +19,9 @@ import numpy as np from qiskit import QuantumCircuit +from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.circuit.library import RealAmplitudes from qiskit_ibm_runtime import Sampler, Session, SamplerV2, SamplerOptions -from qiskit_ibm_runtime.qiskit.primitives import SamplerPub from ..ibm_test_case import IBMTestCase from ..utils import bell