From 7f09810914a156b9494b6279b4f8d75fd57e9747 Mon Sep 17 00:00:00 2001 From: ptristan3 <44805021+ptristan3@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:11:01 -0300 Subject: [PATCH 01/14] Validate precision in `estimator.run()` (#1861) * add precision validation * update message --- qiskit_ibm_runtime/estimator.py | 5 +++++ test/unit/test_estimator.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index aa571622a..18daa09fd 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -189,7 +189,12 @@ def run( Returns: Submitted job. + Raises: + ValueError: if precision value is not strictly greater than 0. """ + if precision is not None: + if precision <= 0: + raise ValueError("The precision value must be strictly greater than 0.") coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs] validate_estimator_pubs(coerced_pubs) return self._run(coerced_pubs) # type: ignore[arg-type] diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index e9fb10d21..723507f4c 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -112,6 +112,17 @@ def test_unsupported_values_for_estimator_options(self): inst.options.update(**bad_opt) self.assertIn(list(bad_opt.keys())[0], str(exc.exception)) + def test_invalid_estimator_precision_option(self): + """Test exception when precision is invalid.""" + + backend = get_mocked_backend() + backend.configuration().simulator = True + + estimator = EstimatorV2(backend=backend) + with self.assertRaises(ValueError) as exc: + estimator.run(**get_primitive_inputs(estimator), precision=0) + self.assertIn("The precision value must be strictly greater than 0", str(exc.exception)) + def test_pec_simulator(self): """Test error is raised when using pec on simulator without coupling map.""" backend = get_mocked_backend() From 8efb3594985f92ce65e20a20dcc7801efb570653 Mon Sep 17 00:00:00 2001 From: Rebecca Dimock <66339736+beckykd@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:31:42 -0500 Subject: [PATCH 02/14] Update batch.py (#1862) * Update resilience_options.py The link in measure_mitgation pointed to measure_noise_learning * Update qiskit_ibm_runtime/options/resilience_options.py * Update qiskit_ibm_runtime/options/resilience_options.py * Update qiskit_ibm_runtime/options/resilience_options.py * remove links for boolean options * fix black * fix lint * update zne/pec wording * Add warning about backend module change (#1841) * Update batch.py Change to an example that runs * Update qiskit_ibm_runtime/batch.py Co-authored-by: Jessie Yu * Update qiskit_ibm_runtime/batch.py Co-authored-by: Jessie Yu * styling --------- Co-authored-by: kevin-tian Co-authored-by: Jessie Yu --- qiskit_ibm_runtime/batch.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/qiskit_ibm_runtime/batch.py b/qiskit_ibm_runtime/batch.py index 3c9d60e1e..ce9a4d9b4 100644 --- a/qiskit_ibm_runtime/batch.py +++ b/qiskit_ibm_runtime/batch.py @@ -46,29 +46,38 @@ class Batch(Session): For example:: - from qiskit.circuit.random import random_circuit + import numpy as np + from qiskit.circuit.library import IQP from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - from qiskit_ibm_runtime import Batch, SamplerV2 as Sampler, QiskitRuntimeService + from qiskit.quantum_info import random_hermitian + from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler, Batch + + n_qubits = 127 service = QiskitRuntimeService() - backend = service.least_busy(operational=True, simulator=False) - pm = generate_preset_pass_manager(backend=backend, optimization_level=1) + backend = service.least_busy(operational=True, simulator=False, min_num_qubits=n_qubits) - # generate fifty unique three-qubit random circuits - circuits = [pm.run(random_circuit(3, 2, measure=True)) for _ in range(50)] + rng = np.random.default_rng() + mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(30)] + circuits = [IQP(mat) for mat in mats] + for circuit in circuits: + circuit.measure_all() + + pm = generate_preset_pass_manager(backend=backend, optimization_level=1) + isa_circuits = pm.run(circuits) - # split up the list of circuits into partitions max_circuits = 10 - partitions = [circuits[i : i + max_circuits] for i in range(0, len(circuits), max_circuits)] + all_partitioned_circuits = [] + for i in range(0, len(isa_circuits), max_circuits): + all_partitioned_circuits.append(isa_circuits[i : i + max_circuits]) + jobs = [] + start_idx = 0 - # run the circuits in batched mode with Batch(backend=backend): sampler = Sampler() - for partition in partitions: - job = sampler.run(partition) - pub_result = job.result()[0] - print(f"Sampler job ID: {job.job_id()}") - print(f"Counts for the first PUB: {pub_result.data.cr.get_counts()}") + for partitioned_circuits in all_partitioned_circuits: + job = sampler.run(partitioned_circuits) + jobs.append(job) For more details, check the "`Run jobs in a batch `_" page. From 2d458dad03279fd738e13f6cde52b5bed62a4940 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 14 Aug 2024 16:36:16 -0400 Subject: [PATCH 03/14] Remove V1 Primitive Support (#1860) * Cherry pick 0.27.1 release notes into main * Remove V1 Primitives * release note & unit test * Clean up v1 options * Docs build * attempt doc fix * Update integration tests --- qiskit_ibm_runtime/__init__.py | 18 +- qiskit_ibm_runtime/base_primitive.py | 225 +--- qiskit_ibm_runtime/estimator.py | 206 +--- .../fake_provider/local_service.py | 124 +-- qiskit_ibm_runtime/options/__init__.py | 40 +- .../options/execution_options.py | 15 - qiskit_ibm_runtime/options/options.py | 244 +---- .../options/resilience_options.py | 42 +- .../options/transpilation_options.py | 75 -- qiskit_ibm_runtime/sampler.py | 173 +--- .../utils/estimator_result_decoder.py | 15 +- qiskit_ibm_runtime/utils/qctrl.py | 83 +- .../utils/sampler_result_decoder.py | 28 +- release-notes/unreleased/1857.other.rst | 2 + test/ibm_test_case.py | 23 +- test/integration/test_estimator.py | 228 ---- test/integration/test_job.py | 79 +- test/integration/test_options.py | 169 --- test/integration/test_retrieve_job.py | 22 +- test/integration/test_sampler.py | 182 ---- test/integration/test_session.py | 39 +- test/qctrl/test_qctrl.py | 374 +------ test/unit/test_estimator.py | 28 +- test/unit/test_ibm_primitives.py | 974 ------------------ test/unit/test_local_mode.py | 92 -- test/unit/test_options.py | 216 +--- test/unit/test_options_utils.py | 34 +- test/unit/test_sampler.py | 29 +- test/utils.py | 20 +- 29 files changed, 83 insertions(+), 3716 deletions(-) delete mode 100644 qiskit_ibm_runtime/options/transpilation_options.py create mode 100644 release-notes/unreleased/1857.other.rst delete mode 100644 test/integration/test_estimator.py delete mode 100644 test/integration/test_options.py delete mode 100644 test/integration/test_sampler.py delete mode 100644 test/unit/test_ibm_primitives.py diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index e6aa4b512..a82c5c419 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -185,11 +185,7 @@ :toctree: ../stubs/ QiskitRuntimeService - Estimator - EstimatorV1 EstimatorV2 - Sampler - SamplerV1 SamplerV2 Session Batch @@ -218,15 +214,15 @@ from .estimator import ( # pylint: disable=reimported EstimatorV2, - EstimatorV1, - EstimatorV1 as Estimator, + EstimatorV2 as Estimator, ) -from .sampler import ( # pylint: disable=reimported - SamplerV2, - SamplerV1, - SamplerV1 as Sampler, +from .sampler import SamplerV2, SamplerV2 as Sampler # pylint: disable=reimported +from .options import ( # pylint: disable=reimported + EstimatorOptions, + SamplerOptions, + OptionsV2, + OptionsV2 as Options, ) -from .options import Options, EstimatorOptions, SamplerOptions, OptionsV2 # Setup the logger for the IBM Quantum Provider package. logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index a1ebaf378..1cf7da14f 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -14,8 +14,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Dict, Optional, Any, Union, TypeVar, Generic, Type -import copy +from typing import Dict, Optional, Union, TypeVar, Generic, Type import logging from dataclasses import asdict, replace import warnings @@ -24,15 +23,10 @@ from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.primitives.containers.sampler_pub import SamplerPub -from qiskit.providers.options import Options as TerraOptions from qiskit.providers.backend import BackendV1, BackendV2 -from .provider_session import get_cm_session as get_cm_provider_session - -from .options import Options from .options.options import BaseOptions, OptionsV2 -from .options.utils import merge_options, set_default_error_levels, merge_options_v2 -from .runtime_job import RuntimeJob +from .options.utils import merge_options, merge_options_v2 from .runtime_job_v2 import RuntimeJobV2 from .ibm_backend import IBMBackend from .utils import validate_isa_circuits, validate_no_dd_with_dynamic_circuits @@ -271,218 +265,3 @@ def _validate_options(self, options: dict) -> None: def _program_id(cls) -> str: """Return the program ID.""" raise NotImplementedError() - - -class BasePrimitiveV1(ABC): - """Base class for Qiskit Runtime primitives.""" - - version = 1 - - def __init__( - self, - backend: Optional[Union[str, BackendV1, BackendV2]] = None, - session: Optional[Session] = None, - options: Optional[Union[Dict, Options]] = None, - ): - """Initializes the primitive. - - Args: - - backend: Backend to run the primitive. This can be a backend name or a ``Backend`` - instance. If a name is specified, the default account (e.g. ``QiskitRuntimeService()``) - is used. - - session: Session in which to call the primitive. - - If both ``session`` and ``backend`` are specified, ``session`` takes precedence. - If neither is specified, and the primitive is created inside a - :class:`qiskit_ibm_runtime.Session` context manager, then the session is used. - Otherwise if IBM Cloud channel is used, a default backend is selected. - - options: Primitive options, see :class:`Options` for detailed description. - The ``backend`` keyword is still supported but is deprecated. - - Raises: - ValueError: Invalid arguments are given. - """ - # `self._options` in this class is a Dict. - # The base class, however, uses a `_run_options` which is an instance of - # qiskit.providers.Options. We largely ignore this _run_options because we use - # a nested dictionary to categorize options. - self._session: Optional[Session] = None - self._service: QiskitRuntimeService | QiskitRuntimeLocalService = None - self._backend: Optional[BackendV1 | BackendV2] = None - - issue_deprecation_msg( - "The Sampler and Estimator V1 primitives have been deprecated", - "0.23.0", - "Please use the V2 Primitives. See the `V2 migration guide " - "`_. for more details", - 3, - ) - - if options is None: - self._options = asdict(Options()) - elif isinstance(options, Options): - self._options = asdict(copy.deepcopy(options)) - else: - options_copy = copy.deepcopy(options) - default_options = asdict(Options()) - self._options = merge_options(default_options, options_copy) - - if isinstance(session, Session): - self._session = session - self._service = self._session.service - self._backend = self._session._backend - return - elif session is not None: # type: ignore[unreachable] - raise ValueError("session must be of type Session or None") - - if isinstance(backend, IBMBackend): # type: ignore[unreachable] - self._service = backend.service - self._backend = backend - elif isinstance(backend, (BackendV1, BackendV2)): - self._service = QiskitRuntimeLocalService() - self._backend = backend - elif isinstance(backend, str): - self._service = ( - QiskitRuntimeService() - if QiskitRuntimeService.global_service is None - else QiskitRuntimeService.global_service - ) - self._backend = self._service.backend(backend) - elif get_cm_session(): - self._session = get_cm_session() - self._service = self._session.service - self._backend = self._service.backend( - name=self._session.backend(), instance=self._session._instance - ) - else: - raise ValueError("A backend or session must be specified.") - - # Check if initialized within a IBMBackend session. If so, issue a warning. - if get_cm_provider_session(): - warnings.warn( - "A Backend.run() session is open but Primitives will not be run within this session" - ) - - def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJob: - """Run the primitive. - - Args: - primitive_inputs: Inputs to pass to the primitive. - user_kwargs: Individual options to overwrite the default primitive options. - - Returns: - Submitted job. - """ - # TODO: Don't check service / backend - if ( - self._backend # pylint: disable=too-many-boolean-expressions - and isinstance(self._backend, IBMBackend) - and isinstance(self._backend.service, QiskitRuntimeService) - and not self._backend.simulator - and getattr(self._backend, "target", None) - and self._service._channel_strategy != "q-ctrl" - ): - validate_isa_circuits(primitive_inputs["circuits"], self._backend.target) - - combined = Options._merge_options(self._options, user_kwargs) - - if self._backend: - combined = set_default_error_levels( - combined, - self._backend, - Options._DEFAULT_OPTIMIZATION_LEVEL, - Options._DEFAULT_RESILIENCE_LEVEL, - ) - else: - combined["optimization_level"] = Options._DEFAULT_OPTIMIZATION_LEVEL - combined["resilience_level"] = Options._DEFAULT_RESILIENCE_LEVEL - - self._validate_options(combined) - - combined = Options._set_default_resilience_options(combined) - combined = Options._remove_none_values(combined) - - primitive_inputs.update(Options._get_program_inputs(combined)) - - if ( - isinstance(self._backend, IBMBackend) - and combined["transpilation"]["skip_transpilation"] - ): - for circ in primitive_inputs["circuits"]: - self._backend.check_faulty(circ) - - logger.info("Submitting job using options %s", combined) - - runtime_options = Options._get_runtime_options(combined) - if self._session: - return self._session.run( - program_id=self._program_id(), - inputs=primitive_inputs, - options=runtime_options, - callback=combined.get("environment", {}).get("callback", None), - result_decoder=DEFAULT_DECODERS.get(self._program_id()), - ) - - if self._backend: - runtime_options["backend"] = self._backend - if "instance" not in runtime_options and isinstance(self._backend, IBMBackend): - runtime_options["instance"] = self._backend._instance - - if isinstance(self._service, QiskitRuntimeService): - return self._service.run( - program_id=self._program_id(), # type: ignore[arg-type] - options=runtime_options, - inputs=primitive_inputs, - callback=combined.get("environment", {}).get("callback", None), - result_decoder=DEFAULT_DECODERS.get(self._program_id()), - ) - return self._service._run( # type: ignore[call-arg] - program_id=self._program_id(), # type: ignore[arg-type] - options=runtime_options, - inputs=primitive_inputs, - ) - - @property - def session(self) -> Optional[Session]: - """Return session used by this primitive. - - Returns: - Session used by this primitive, or ``None`` if session is not used. - """ - return self._session - - @property - def options(self) -> TerraOptions: - """Return options values for the sampler. - Returns: - options - """ - return TerraOptions(**self._options) - - def set_options(self, **fields: Any) -> None: - """Set options values for the sampler. - - Args: - **fields: The fields to update the options - """ - self._options = merge_options( # pylint: disable=attribute-defined-outside-init - self._options, fields - ) - - @abstractmethod - def _validate_options(self, options: dict) -> None: - """Validate that primitive inputs (options) are valid - - Raises: - ValueError: if resilience_level is out of the allowed range. - """ - raise NotImplementedError() - - @classmethod - @abstractmethod - def _program_id(cls) -> str: - """Return the program ID.""" - raise NotImplementedError() diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 18daa09fd..d6049e03c 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -13,27 +13,20 @@ """Estimator primitive.""" from __future__ import annotations -import os -from typing import Optional, Dict, Sequence, Any, Union, Iterable + +from typing import Optional, Dict, Union, Iterable import logging -from qiskit.circuit import QuantumCircuit from qiskit.providers import BackendV1, BackendV2 -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 .runtime_job_v2 import RuntimeJobV2 -from .ibm_backend import IBMBackend -from .options import Options from .options.estimator_options import EstimatorOptions -from .base_primitive import BasePrimitiveV1, BasePrimitiveV2 +from .base_primitive import BasePrimitiveV2 from .utils.deprecation import deprecate_arguments, issue_deprecation_msg -from .utils.qctrl import validate as qctrl_validate from .utils.qctrl import validate_v2 as qctrl_validate_v2 from .utils import validate_estimator_pubs @@ -235,194 +228,3 @@ def _validate_options(self, options: dict) -> None: def _program_id(cls) -> str: """Return the program ID.""" return "estimator" - - -class EstimatorV1(BasePrimitiveV1, Estimator, BaseEstimator): - """Class for interacting with Qiskit Runtime Estimator primitive service. - - .. deprecated:: 0.23 - The ``EstimatorV1`` primitives have been deprecated in 0.23, released on April 15, 2024. - See the `V2 migration guide `_. - for more details. - The ``EstimatorV1`` support will be removed no earlier than July 15, 2024. - - Qiskit Runtime Estimator primitive service estimates expectation values of quantum circuits and - observables. - - 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. - - Example:: - - from qiskit.circuit.library import RealAmplitudes - from qiskit.quantum_info import SparsePauliOp - - from qiskit_ibm_runtime import QiskitRuntimeService, Estimator - - service = QiskitRuntimeService(channel="ibm_cloud") - - psi1 = RealAmplitudes(num_qubits=2, reps=2) - - H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - H2 = SparsePauliOp.from_list([("IZ", 1)]) - H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) - - with Session(service=service, backend="ibmq_qasm_simulator") as session: - estimator = Estimator(session=session) - - theta1 = [0, 1, 1, 2, 3, 5] - - # calculate [ ] - psi1_H1 = estimator.run(circuits=[psi1], observables=[H1], parameter_values=[theta1]) - print(psi1_H1.result()) - - # calculate [ , ] - psi1_H23 = estimator.run( - circuits=[psi1, psi1], - observables=[H2, H3], - parameter_values=[theta1]*2 - ) - print(psi1_H23.result()) - """ - - version = 1 - - def __init__( - self, - backend: Optional[Union[str, IBMBackend]] = None, - session: Optional[Session] = None, - options: Optional[Union[Dict, Options]] = None, - ): - """Initializes the Estimator primitive. - - Args: - backend: Backend to run the primitive. This can be a backend name or an :class:`IBMBackend` - instance. If a name is specified, the default account (e.g. ``QiskitRuntimeService()``) - is used. - - session: Session in which to call the primitive. - - If both ``session`` and ``backend`` are specified, ``session`` takes precedence. - If neither is specified, and the primitive is created inside a - :class:`qiskit_ibm_runtime.Session` context manager, then the session is used. - Otherwise if IBM Cloud channel is used, a default backend is selected. - - options: Primitive options, see :class:`Options` for detailed description. - The ``backend`` keyword is still supported but is deprecated. - """ - # `self._options` in this class is a Dict. - # The base class, however, uses a `_run_options` which is an instance of - # qiskit.providers.Options. We largely ignore this _run_options because we use - # a nested dictionary to categorize options. - BaseEstimator.__init__(self) - Estimator.__init__(self) - BasePrimitiveV1.__init__(self, backend=backend, session=session, options=options) - - def run( # pylint: disable=arguments-differ - self, - circuits: QuantumCircuit | Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | str] | BaseOperator | str, - parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, - **kwargs: Any, - ) -> RuntimeJob: - """Submit a request to the estimator primitive. - - Args: - circuits: a (parameterized) :class:`~qiskit.circuit.QuantumCircuit` or - a list of (parameterized) :class:`~qiskit.circuit.QuantumCircuit`. - - observables: Observable objects. - - parameter_values: Concrete parameters to be bound. - - **kwargs: Individual options to overwrite the default primitive options. - - Returns: - Submitted job. - The result of the job is an instance of :class:`qiskit.primitives.EstimatorResult`. - - Raises: - ValueError: Invalid arguments are given. - """ - # To bypass base class merging of options. - user_kwargs = {"_user_kwargs": kwargs} - return super().run( - circuits=circuits, - observables=observables, - parameter_values=parameter_values, - **user_kwargs, - ) - - def _run( # pylint: disable=arguments-differ - self, - circuits: tuple[QuantumCircuit, ...], - observables: tuple[SparsePauliOp, ...], - parameter_values: tuple[tuple[float, ...], ...], - **kwargs: Any, - ) -> RuntimeJob: - """Submit a request to the estimator primitive. - - Args: - circuits: a (parameterized) :class:`~qiskit.circuit.QuantumCircuit` or - a list of (parameterized) :class:`~qiskit.circuit.QuantumCircuit`. - - observables: A list of observable objects. - - parameter_values: An optional list of concrete parameters to be bound. - - **kwargs: Individual options to overwrite the default primitive options. - - Returns: - Submitted job - """ - inputs = { - "circuits": circuits, - "observables": observables, - "parameters": [circ.parameters for circ in circuits], - "parameter_values": parameter_values, - } - return self._run_primitive( - primitive_inputs=inputs, user_kwargs=kwargs.get("_user_kwargs", {}) - ) - - def _validate_options(self, options: dict) -> None: - """Validate that primitive inputs (options) are valid - Raises: - ValueError: if resilience_level is out of the allowed range. - ValueError: if resilience_level==3, backend is simulator and no coupling map - """ - if os.getenv("QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"): - return - - if self._service._channel_strategy == "q-ctrl": - qctrl_validate(options) - return - - if not options.get("resilience_level") in list( - range(Options._MAX_RESILIENCE_LEVEL_ESTIMATOR + 1) - ): - raise ValueError( - f"resilience_level can only take the values " - f"{list(range(Options._MAX_RESILIENCE_LEVEL_ESTIMATOR + 1))} in Estimator" - ) - - if ( - options.get("resilience_level") == 3 - and self._backend - and self._backend.configuration().simulator - ): - if not options.get("simulator").get("coupling_map"): - raise ValueError( - "When the backend is a simulator and resilience_level == 3," - "a coupling map is required." - ) - Options.validate_options(options) - - @classmethod - def _program_id(cls) -> str: - """Return the program ID.""" - return "estimator" diff --git a/qiskit_ibm_runtime/fake_provider/local_service.py b/qiskit_ibm_runtime/fake_provider/local_service.py index b16c01323..f1a072b81 100644 --- a/qiskit_ibm_runtime/fake_provider/local_service.py +++ b/qiskit_ibm_runtime/fake_provider/local_service.py @@ -22,16 +22,13 @@ from typing import Callable, Dict, List, Literal, Optional, Union from qiskit.primitives import ( - BackendEstimator, BackendEstimatorV2, - BackendSampler, BackendSamplerV2, ) from qiskit.primitives.primitive_job import PrimitiveJob from qiskit.providers.backend import BackendV1, BackendV2 from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.providerutils import filter_backends -from qiskit.utils import optionals from .fake_backend import FakeBackendV2 # pylint: disable=cyclic-import from .fake_provider import FakeProviderForBackendV2 # pylint: disable=unused-import, cyclic-import @@ -188,123 +185,14 @@ def _run( ) inputs = copy.deepcopy(inputs) - primitive_version = inputs.pop("version", 1) - if primitive_version == 1: - primitive_inputs = { - "circuits": inputs.pop("circuits"), - "parameter_values": inputs.pop("parameter_values"), - } - if program_id == "estimator": - primitive_inputs["observables"] = inputs.pop("observables") - inputs.pop("parameters", None) - - if optionals.HAS_AER: - # pylint: disable=import-outside-toplevel - from qiskit_aer.backends.aerbackend import AerBackend - - if isinstance(backend, AerBackend): - return self._run_aer_primitive_v1( - primitive=program_id, options=inputs, inputs=primitive_inputs - ) - - return self._run_backend_primitive_v1( - backend=backend, - primitive=program_id, - options=inputs, - inputs=primitive_inputs, - ) - else: - primitive_inputs = {"pubs": inputs.pop("pubs")} - return self._run_backend_primitive_v2( - backend=backend, - primitive=program_id, - options=inputs.get("options", {}), - inputs=primitive_inputs, - ) - - def _run_aer_primitive_v1( - self, primitive: Literal["sampler", "estimator"], options: dict, inputs: dict - ) -> PrimitiveJob: - """Run V1 Aer primitive. - Args: - primitive: Name of the primitive. - options: Primitive options to use. - inputs: Primitive inputs. - - Returns: - The job object of the result of the primitive. - """ - # pylint: disable=import-outside-toplevel - from qiskit_aer.primitives import Estimator, Sampler - - # TODO: issue warning if extra options are used - options_copy = copy.deepcopy(options) - transpilation_options = options_copy.get("transpilation_settings", {}) - skip_transpilation = transpilation_options.pop("skip_transpilation", False) - optimization_level = transpilation_options.pop("optimization_settings", {}).pop( - "level", None + primitive_inputs = {"pubs": inputs.pop("pubs")} + return self._run_backend_primitive_v2( + backend=backend, + primitive=program_id, + options=inputs.get("options", {}), + inputs=primitive_inputs, ) - transpilation_options["optimization_level"] = optimization_level - input_run_options = options_copy.get("run_options", {}) - run_options = { - "shots": input_run_options.pop("shots", None), - "seed_simulator": input_run_options.pop("seed_simulator", None), - } - backend_options = {"noise_model": input_run_options.pop("noise_model", None)} - - if primitive == "sampler": - primitive_inst = Sampler( - backend_options=backend_options, - transpile_options=transpilation_options, - run_options=run_options, - skip_transpilation=skip_transpilation, - ) - else: - primitive_inst = Estimator( - backend_options=backend_options, - transpile_options=transpilation_options, - run_options=run_options, - skip_transpilation=skip_transpilation, - ) - return primitive_inst.run(**inputs) - - def _run_backend_primitive_v1( - self, - backend: BackendV1 | BackendV2, - primitive: Literal["sampler", "estimator"], - options: dict, - inputs: dict, - ) -> PrimitiveJob: - """Run V1 backend primitive. - - Args: - backend: The backend to run the primitive on. - primitive: Name of the primitive. - options: Primitive options to use. - inputs: Primitive inputs. - - Returns: - The job object of the result of the primitive. - """ - options_copy = copy.deepcopy(options) - transpilation_options = options_copy.get("transpilation_settings", {}) - skip_transpilation = transpilation_options.pop("skip_transpilation", False) - optimization_level = transpilation_options.pop("optimization_settings", {}).get("level") - transpilation_options["optimization_level"] = optimization_level - input_run_options = options.get("run_options", {}) - run_options = { - "shots": input_run_options.get("shots"), - "seed_simulator": input_run_options.get("seed_simulator"), - "noise_model": input_run_options.get("noise_model"), - } - if primitive == "sampler": - primitive_inst = BackendSampler(backend=backend, skip_transpilation=skip_transpilation) - else: - primitive_inst = BackendEstimator(backend=backend) - - primitive_inst.set_transpile_options(**transpilation_options) - return primitive_inst.run(**inputs, **run_options) def _run_backend_primitive_v2( self, diff --git a/qiskit_ibm_runtime/options/__init__.py b/qiskit_ibm_runtime/options/__init__.py index 3252f8bd3..c9aa3f938 100644 --- a/qiskit_ibm_runtime/options/__init__.py +++ b/qiskit_ibm_runtime/options/__init__.py @@ -49,29 +49,6 @@ default values are subject to change. Refer to this current module's documentation for the latest defaults. - -V1 Primitives -============= - -The :class:`Options` class encapsulates all the options you can specify -when invoking a V1 primitive. It includes frequently used options, -such as ``optimization_level`` and ``resilience_level`` as well as -sub-categories, such as ``transpilation`` and ``execution``. -You can use auto-complete to easily find the options inside each -sub-category, for example:: - - from qiskit_ibm_runtime.options import Options - - options = Options() - options.transpilation.initial_layout = [0, 1, 2, 3] # This an be done using auto-complete - -You can also pass dictionaries to each sub-category, for example:: - - from qiskit_ibm_runtime.options import Options - - options = Options(transpilation={"initial_layout": [0, 1, 2, 3]}) - - Classes ======= @@ -83,7 +60,6 @@ EstimatorOptions SamplerOptions - Options Suboptions for V2 primitives only @@ -112,27 +88,13 @@ EnvironmentOptions SimulatorOptions - -Suboptions for V1 primitives only ---------------------------------- - -.. autosummary:: - :toctree: ../stubs/ - - TranspilationOptions - ExecutionOptions - ResilienceOptions - """ from .environment_options import EnvironmentOptions -from .execution_options import ExecutionOptions from .execution_options import ExecutionOptionsV2 from .noise_learner_options import NoiseLearnerOptions -from .options import Options, OptionsV2 +from .options import OptionsV2 from .simulator_options import SimulatorOptions -from .transpilation_options import TranspilationOptions -from .resilience_options import ResilienceOptions from .resilience_options import ResilienceOptionsV2 from .twirling_options import TwirlingOptions from .estimator_options import EstimatorOptions diff --git a/qiskit_ibm_runtime/options/execution_options.py b/qiskit_ibm_runtime/options/execution_options.py index 8208a0af5..25f0906fc 100644 --- a/qiskit_ibm_runtime/options/execution_options.py +++ b/qiskit_ibm_runtime/options/execution_options.py @@ -32,18 +32,3 @@ class ExecutionOptionsV2: range supplied by ``backend.rep_delay_range``. Default is given by ``backend.default_rep_delay``. """ - - -@primitive_dataclass -class ExecutionOptions: - """Execution options for V1 primitives. - - Args: - shots: Number of repetitions of each circuit, for sampling. Default: 4000. - - init_qubits: Whether to reset the qubits to the ground state for each shot. - Default: ``True``. - """ - - shots: int = 4000 - init_qubits: bool = True diff --git a/qiskit_ibm_runtime/options/options.py b/qiskit_ibm_runtime/options/options.py index 88f325aa2..f39b289a4 100644 --- a/qiskit_ibm_runtime/options/options.py +++ b/qiskit_ibm_runtime/options/options.py @@ -13,17 +13,15 @@ """Primitive options.""" from abc import abstractmethod -from typing import Iterable, Optional, Tuple, Union, ClassVar, Any -from dataclasses import dataclass, fields, field, asdict, is_dataclass +from typing import Iterable, Tuple, Union, Any +from dataclasses import dataclass, fields, asdict, is_dataclass import copy -import warnings from qiskit.transpiler import CouplingMap from pydantic import Field, ValidationError from .utils import ( Dict, - _to_obj, UnsetType, Unset, remove_dict_unset_values, @@ -33,10 +31,7 @@ remove_empty_dict, ) from .environment_options import EnvironmentOptions -from .execution_options import ExecutionOptions from .simulator_options import SimulatorOptions -from .transpilation_options import TranspilationOptions -from .resilience_options import ResilienceOptions from ..runtime_options import RuntimeOptions from ..utils.deprecation import issue_deprecation_msg @@ -229,238 +224,3 @@ def _set_if_exists(name: str, _inputs: dict, _options: dict) -> None: inputs["resilience_level"] = options_copy["resilience_level"] return inputs - - -@dataclass -class Options(BaseOptions): - """Options for V1 primitives. - - Args: - optimization_level: How much optimization to perform on the circuits. - Higher levels generate more optimized circuits, - at the expense of longer transpilation times. This is based on the - ``optimization_level`` parameter in qiskit-terra but may include - backend-specific optimization. Default: 3. - - * 0: no optimization - * 1: light optimization - * 2: heavy optimization - * 3: even heavier optimization - - resilience_level: How much resilience to build against errors. - Higher levels generate more accurate results, - at the expense of longer processing times. Default: 1. - - * 0: No mitigation. - * 1: Minimal mitigation costs. Mitigate error associated with readout errors. - * 2: Medium mitigation costs. Typically reduces bias in estimators but - is not guaranteed to be zero bias. Only applies to estimator. - * 3: Heavy mitigation with layer sampling. Theoretically expected to deliver zero - bias estimators. Only applies to estimator. - - Refer to the - `Qiskit Runtime documentation - `_. - for more information about the error mitigation methods used at each level. - - max_execution_time: Maximum execution time in seconds, which is based - on system execution time (not wall clock time). System execution time is - the amount of time that the system is dedicated to processing your job. - If a job exceeds this time limit, it is forcibly cancelled. - Simulator jobs continue to use wall clock time. - - Refer to the - `Max execution time documentation - `_. - for more information. - - transpilation: Transpilation options. See :class:`TranspilationOptions` for all - available options. - - resilience: Advanced resilience options to fine tune the resilience strategy. - See :class:`ResilienceOptions` for all available options. - - execution: Execution time options. See :class:`ExecutionOptions` for all available options. - - environment: Options related to the execution environment. See - :class:`EnvironmentOptions` for all available options. - - simulator: Simulator options. See - :class:`SimulatorOptions` for all available options. - """ - - # Defaults for optimization_level and for resilience_level will be assigned - # in Sampler/Estimator - _DEFAULT_OPTIMIZATION_LEVEL = 1 - _DEFAULT_RESILIENCE_LEVEL = 1 - _MAX_OPTIMIZATION_LEVEL = 3 - _MAX_RESILIENCE_LEVEL_ESTIMATOR = 3 - _MAX_RESILIENCE_LEVEL_SAMPLER = 1 - _MAX_EXECUTION_TIME = 8 * 60 * 60 # 8 hours for real device - - optimization_level: Optional[int] = None - resilience_level: Optional[int] = None - max_execution_time: Optional[int] = None - transpilation: Union[TranspilationOptions, Dict] = field(default_factory=TranspilationOptions) - resilience: Union[ResilienceOptions, Dict] = field(default_factory=ResilienceOptions) - execution: Union[ExecutionOptions, Dict] = field(default_factory=ExecutionOptions) - environment: Union[EnvironmentOptions, Dict] = field(default_factory=EnvironmentOptions) - simulator: Union[SimulatorOptions, Dict] = field(default_factory=SimulatorOptions) - - _obj_fields: ClassVar[dict] = { - "transpilation": TranspilationOptions, - "execution": ExecutionOptions, - "environment": EnvironmentOptions, - "simulator": SimulatorOptions, - "resilience": ResilienceOptions, - } - - def __post_init__(self): # type: ignore - """Convert dictionary fields to object.""" - obj_fields = getattr(self, "_obj_fields", {}) - for key in list(obj_fields): - if hasattr(self, key): - orig_val = getattr(self, key) - setattr(self, key, _to_obj(obj_fields[key], orig_val)) - - @staticmethod - def _get_program_inputs(options: dict) -> dict: - """Convert the input options to program compatible inputs. - - Returns: - Inputs acceptable by primitives. - """ - sim_options = options.get("simulator", {}) - inputs = {} - inputs["transpilation_settings"] = options.get("transpilation", {}) - inputs["transpilation_settings"].update( - { - "optimization_settings": {"level": options.get("optimization_level")}, - "coupling_map": sim_options.get("coupling_map", None), - "basis_gates": sim_options.get("basis_gates", None), - } - ) - if isinstance(inputs["transpilation_settings"]["coupling_map"], CouplingMap): - inputs["transpilation_settings"]["coupling_map"] = list( - map(list, inputs["transpilation_settings"]["coupling_map"].get_edges()) - ) - - inputs["resilience_settings"] = options.get("resilience", {}) - inputs["resilience_settings"].update({"level": options.get("resilience_level")}) - inputs["run_options"] = options.get("execution") - inputs["run_options"].update( - { - "noise_model": sim_options.get("noise_model", None), - "seed_simulator": sim_options.get("seed_simulator", None), - } - ) - - known_keys = list(Options.__dataclass_fields__.keys()) - known_keys.append("image") - # Add additional unknown keys. - for key in options.keys(): - if key not in known_keys: - warnings.warn(f"Key '{key}' is an unrecognized option. It may be ignored.") - inputs[key] = options[key] - remove_dict_unset_values(inputs) - return inputs - - @staticmethod - def validate_options(options: dict) -> None: - """Validate that program inputs (options) are valid - Raises: - ValueError: if optimization_level is outside the allowed range. - ValueError: if max_execution_time is outside the allowed range. - """ - if not options.get("optimization_level") in list( - range(Options._MAX_OPTIMIZATION_LEVEL + 1) - ): - raise ValueError( - f"optimization_level can only take the values " - f"{list(range(Options._MAX_OPTIMIZATION_LEVEL + 1))}" - ) - ResilienceOptions(**options.get("resilience", {})) - TranspilationOptions(**options.get("transpilation", {})) - execution_time = options.get("max_execution_time") - if execution_time is not None: - if execution_time > Options._MAX_EXECUTION_TIME: - raise ValueError( - f"max_execution_time must be below " - f"{Options._MAX_EXECUTION_TIME} seconds." - f"max_execution_time must be below " - f"{Options._MAX_EXECUTION_TIME} seconds." - ) - - EnvironmentOptions(**options.get("environment", {})) - ExecutionOptions(**options.get("execution", {})) - SimulatorOptions(**options.get("simulator", {})) - - @staticmethod - def _remove_none_values(options: dict) -> dict: - """Remove `None` values from the options dictionary.""" - new_options = {} - for key, value in options.items(): - if value is not None: - if isinstance(value, dict): - new_suboptions = {} - for subkey, subvalue in value.items(): - if subvalue is not None: - new_suboptions[subkey] = subvalue - new_options[key] = new_suboptions - else: - new_options[key] = value - - return new_options - - @staticmethod - def _set_default_resilience_options(options: dict) -> dict: - """Set default resilience options for resilience level 2.""" - if options["resilience_level"] == 2: - if not options["resilience"]["noise_factors"]: - options["resilience"]["noise_factors"] = (1, 3, 5) - if not options["resilience"]["extrapolator"]: - options["resilience"]["extrapolator"] = "LinearExtrapolator" - - return options - - @staticmethod - def _merge_options(old_options: dict, new_options: Optional[dict] = None) -> dict: - """Merge current options with the new ones. - - Args: - new_options: New options to merge. - - Returns: - Merged dictionary. - """ - - def _update_options(old: dict, new: dict, matched: Optional[dict] = None) -> None: - if not new and not matched: - return - matched = matched or {} - - for key, val in old.items(): - if isinstance(val, dict): - matched = new.pop(key, {}) - _update_options(val, new, matched) - elif key in new.keys(): - old[key] = new.pop(key) - elif key in matched.keys(): - old[key] = matched.pop(key) - - # Add new keys. - for key, val in matched.items(): - old[key] = val - - combined = copy.deepcopy(old_options) - if not new_options: - return combined - new_options_copy = copy.deepcopy(new_options) - - # First update values of the same key. - _update_options(combined, new_options_copy) - - # Add new keys. - combined.update(new_options_copy) - - return combined diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py index b585686b5..0b22d06da 100644 --- a/qiskit_ibm_runtime/options/resilience_options.py +++ b/qiskit_ibm_runtime/options/resilience_options.py @@ -12,7 +12,7 @@ """Resilience options.""" -from typing import Sequence, Literal, Union, Optional +from typing import Literal, Union from dataclasses import asdict from pydantic import model_validator, Field @@ -97,43 +97,3 @@ def _validate_options(self) -> "ResilienceOptionsV2": ) return self - - -@primitive_dataclass -class ResilienceOptions: - """Resilience options for V1 primitives. - - Args: - noise_factors: An list of real valued noise factors that determine by what amount the - circuits' noise is amplified. - Only applicable for ``resilience_level=2``. - Default: ``None``, and (1, 3, 5) if resilience level is 2. - - noise_amplifier: A noise amplification strategy. Currently only - ``"LocalFoldingAmplifier"`` is supported Only applicable for ``resilience_level=2``. - Default: "LocalFoldingAmplifier". - - extrapolator: An extrapolation strategy. One of ``"LinearExtrapolator"``, - ``"QuadraticExtrapolator"``, ``"CubicExtrapolator"``, ``"QuarticExtrapolator"``. - Note that ``"CubicExtrapolator"`` and ``"QuarticExtrapolator"`` require more - noise factors than the default. - Only applicable for ``resilience_level=2``. - Default: ``None``, and ``LinearExtrapolator`` if resilience level is 2. - """ - - noise_amplifier: Optional[NoiseAmplifierType] = None - noise_factors: Optional[Sequence[float]] = None - extrapolator: Optional[ExtrapolatorType] = None - - @model_validator(mode="after") - def _validate_options(self) -> "ResilienceOptions": - """Validate the model.""" - required_factors = { - "QuarticExtrapolator": 5, - "CubicExtrapolator": 4, - } - req_len = required_factors.get(self.extrapolator, None) - if req_len and len(self.noise_factors) < req_len: - raise ValueError(f"{self.extrapolator} requires at least {req_len} noise_factors.") - - return self diff --git a/qiskit_ibm_runtime/options/transpilation_options.py b/qiskit_ibm_runtime/options/transpilation_options.py deleted file mode 100644 index c772c24d4..000000000 --- a/qiskit_ibm_runtime/options/transpilation_options.py +++ /dev/null @@ -1,75 +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. - -"""Transpilation options.""" - -from typing import List, Union, Literal - -from pydantic import field_validator - -from .utils import Unset, UnsetType, skip_unset_validation, primitive_dataclass - - -LayoutMethodType = Literal[ - "trivial", - "dense", - "noise_adaptive", - "sabre", -] -RoutingMethodType = Literal[ - "basic", - "lookahead", - "stochastic", - "sabre", - "none", -] -MAX_OPTIMIZATION_LEVEL: int = 1 - - -@primitive_dataclass -class TranspilationOptions: - """Transpilation options. This is only used by V1 primitives. - - Args: - - skip_transpilation: Whether to skip transpilation. Default is False. - - initial_layout: Initial position of virtual qubits on physical qubits. - See ``qiskit.compiler.transpile`` for more information. - - layout_method: Name of layout selection pass. One of - 'trivial', 'dense', 'noise_adaptive', 'sabre'. - - routing_method: Name of routing pass. - One of 'basic', 'lookahead', 'stochastic', 'sabre', 'none'. - - approximation_degree: heuristic dial used for circuit approximation - (1.0=no approximation, 0.0=maximal approximation) - """ - - skip_transpilation: bool = False - initial_layout: Union[UnsetType, dict, List] = Unset # TODO: Support Layout - layout_method: Union[UnsetType, LayoutMethodType] = Unset - routing_method: Union[UnsetType, RoutingMethodType] = Unset - approximation_degree: Union[UnsetType, float] = Unset - - @field_validator("approximation_degree") - @classmethod - @skip_unset_validation - def _validate_approximation_degree(cls, degree: float) -> float: - """Validate approximation_degree.""" - if not 0.0 <= degree <= 1.0: - raise ValueError( - "approximation_degree must be between 0.0 (maximal approximation) " - "and 1.0 (no approximation)" - ) - return degree diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py index 9232dc3b5..ee05fe71e 100644 --- a/qiskit_ibm_runtime/sampler.py +++ b/qiskit_ibm_runtime/sampler.py @@ -13,27 +13,23 @@ """Sampler primitive.""" from __future__ import annotations -import os -from typing import Dict, Optional, Sequence, Any, Union, Iterable + +from typing import Dict, Optional, Union, Iterable import logging -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 qiskit.providers import BackendV1, BackendV2 -from .options import Options -from .runtime_job import RuntimeJob + from .runtime_job_v2 import RuntimeJobV2 -from .ibm_backend import IBMBackend -from .base_primitive import BasePrimitiveV1, BasePrimitiveV2 +from .base_primitive import BasePrimitiveV2 # pylint: disable=unused-import,cyclic-import from .session import Session from .batch import Batch from .utils.deprecation import deprecate_arguments, issue_deprecation_msg -from .utils.qctrl import validate as qctrl_validate from .utils.qctrl import validate_v2 as qctrl_validate_v2 from .utils import validate_classical_registers from .options import SamplerOptions @@ -174,162 +170,3 @@ def _validate_options(self, options: dict) -> None: def _program_id(cls) -> str: """Return the program ID.""" return "sampler" - - -class SamplerV1(BasePrimitiveV1, Sampler, BaseSampler): - """Class for interacting with Qiskit Runtime Sampler primitive service. - - .. deprecated:: 0.23 - The ``SamplerV1`` primitives have been deprecated in 0.23, released on April 15, 2024. - See the `V2 migration guide `_. - for more details. - The ``SamplerV1`` support will be removed no earlier than July 15, 2024. - - Qiskit Runtime Sampler primitive service calculates quasi-probability distribution - of bitstrings from quantum circuits. - - The :meth:`run` method can be used to submit circuits and parameters to the Sampler 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. - - Example:: - - from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister - from qiskit_ibm_runtime import QiskitRuntimeService, Session, Sampler - - service = QiskitRuntimeService(channel="ibm_cloud") - - # Bell Circuit - qr = QuantumRegister(2, name="qr") - cr = ClassicalRegister(2, name="cr") - qc = QuantumCircuit(qr, cr, name="bell") - qc.h(qr[0]) - qc.cx(qr[0], qr[1]) - qc.measure(qr, cr) - - with Session(service, backend="ibmq_qasm_simulator") as session: - sampler = Sampler(session=session) - - job = sampler.run(qc, shots=1024) - print(f"Job ID: {job.job_id()}") - print(f"Job result: {job.result()}") - - # You can run more jobs inside the session - """ - - _options_class = Options - - version = 1 - - def __init__( - self, - backend: Optional[Union[str, IBMBackend]] = None, - session: Optional[Session] = None, - options: Optional[Union[Dict, Options]] = None, - ): - """Initializes the Sampler primitive. - - Args: - backend: Backend to run the primitive. This can be a backend name or an :class:`IBMBackend` - instance. If a name is specified, the default account (e.g. ``QiskitRuntimeService()``) - is used. - - session: Session in which to call the primitive. - - If both ``session`` and ``backend`` are specified, ``session`` takes precedence. - If neither is specified, and the primitive is created inside a - :class:`qiskit_ibm_runtime.Session` context manager, then the session is used. - Otherwise if IBM Cloud channel is used, a default backend is selected. - - options: Primitive options, see :class:`Options` for detailed description. - The ``backend`` keyword is still supported but is deprecated. - """ - # `self._options` in this class is a Dict. - # The base class, however, uses a `_run_options` which is an instance of - # qiskit.providers.Options. We largely ignore this _run_options because we use - # a nested dictionary to categorize options. - BaseSampler.__init__(self) - Sampler.__init__(self) - BasePrimitiveV1.__init__(self, backend=backend, session=session, options=options) - - def run( # pylint: disable=arguments-differ - self, - circuits: QuantumCircuit | Sequence[QuantumCircuit], - parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, - **kwargs: Any, - ) -> RuntimeJob: - """Submit a request to the sampler primitive. - - Args: - circuits: A (parameterized) :class:`~qiskit.circuit.QuantumCircuit` or - a list of (parameterized) :class:`~qiskit.circuit.QuantumCircuit`. - parameter_values: Concrete parameters to be bound. - **kwargs: Individual options to overwrite the default primitive options. - - Returns: - Submitted job. - The result of the job is an instance of :class:`qiskit.primitives.SamplerResult`. - - Raises: - ValueError: Invalid arguments are given. - """ - # To bypass base class merging of options. - user_kwargs = {"_user_kwargs": kwargs} - return super().run( - circuits=circuits, - parameter_values=parameter_values, - **user_kwargs, - ) - - def _run( # pylint: disable=arguments-differ - self, - circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], - **kwargs: Any, - ) -> RuntimeJob: - """Submit a request to the sampler primitive. - - Args: - circuits: A (parameterized) :class:`~qiskit.circuit.QuantumCircuit` or - a list of (parameterized) :class:`~qiskit.circuit.QuantumCircuit`. - parameter_values: An optional list of concrete parameters to be bound. - **kwargs: Individual options to overwrite the default primitive options. - - Returns: - Submitted job. - """ - inputs = { - "circuits": circuits, - "parameters": [circ.parameters for circ in circuits], - "parameter_values": parameter_values, - } - return self._run_primitive( - primitive_inputs=inputs, user_kwargs=kwargs.get("_user_kwargs", {}) - ) - - def _validate_options(self, options: dict) -> None: - """Validate that primitive inputs (options) are valid - Raises: - ValueError: if resilience_level is out of the allowed range. - """ - if os.getenv("QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"): - return - - if self._service._channel_strategy == "q-ctrl": - qctrl_validate(options) - return - - valid_levels = list(range(Options._MAX_RESILIENCE_LEVEL_SAMPLER + 1)) - if options.get("resilience_level") and not options.get("resilience_level") in valid_levels: - raise ValueError( - f"resilience_level {options.get('resilience_level')} is not a valid value." - f"It can only take the values {valid_levels} in Sampler." - ) - Options.validate_options(options) - - @classmethod - def _program_id(cls) -> str: - """Return the program ID.""" - return "sampler" diff --git a/qiskit_ibm_runtime/utils/estimator_result_decoder.py b/qiskit_ibm_runtime/utils/estimator_result_decoder.py index 5a59bf45f..6f32b369f 100644 --- a/qiskit_ibm_runtime/utils/estimator_result_decoder.py +++ b/qiskit_ibm_runtime/utils/estimator_result_decoder.py @@ -12,10 +12,8 @@ """Estimator result decoder.""" -from typing import Dict, Union -import numpy as np +from typing import Dict -from qiskit.primitives import EstimatorResult from qiskit.primitives.containers import PrimitiveResult from .result_decoder import ResultDecoder @@ -27,13 +25,8 @@ class EstimatorResultDecoder(ResultDecoder): @classmethod def decode( # type: ignore # pylint: disable=arguments-differ cls, raw_result: str - ) -> Union[EstimatorResult, PrimitiveResult]: + ) -> PrimitiveResult: """Convert the result to EstimatorResult.""" decoded: Dict = super().decode(raw_result) - if isinstance(decoded, PrimitiveResult): - return decoded - else: - return EstimatorResult( - values=np.asarray(decoded["values"]), - metadata=decoded["metadata"], - ) + + return decoded diff --git a/qiskit_ibm_runtime/utils/qctrl.py b/qiskit_ibm_runtime/utils/qctrl.py index 852652cde..4210d0711 100644 --- a/qiskit_ibm_runtime/utils/qctrl.py +++ b/qiskit_ibm_runtime/utils/qctrl.py @@ -15,86 +15,12 @@ import logging from typing import Any, Optional, Dict, List -from ..options import Options -from ..options import EnvironmentOptions, ExecutionOptions, TranspilationOptions, SimulatorOptions +from ..options import EnvironmentOptions, SimulatorOptions from ..options.utils import UnsetType logger = logging.getLogger(__name__) -def validate(options: Dict[str, Any]) -> None: - """Validates the options for qctrl""" - - # Raise error on bad options. - _raise_if_error_in_options(options) - # Override options and warn. - _warn_and_clean_options(options) - - # Default validation otherwise. - TranspilationOptions(**options.get("transpilation", {})) - execution_time = options.get("max_execution_time") - if execution_time is not None: - if execution_time > Options._MAX_EXECUTION_TIME: - raise ValueError( - f"max_execution_time must be below " f"{Options._MAX_EXECUTION_TIME} seconds." - ) - - EnvironmentOptions(**options.get("environment", {})) - ExecutionOptions(**options.get("execution", {})) - SimulatorOptions(**options.get("simulator", {})) - - -def _raise_if_error_in_options(options: Dict[str, Any]) -> None: - """Checks for settings that produce errors and raise a ValueError""" - - # Fail on resilience_level set to 0 - resilience_level = options.get("resilience_level", 1) - _check_argument( - resilience_level > 0, - description=( - "Q-CTRL Primitives do not support resilience level 0. Please " - "set resilience_level to 1 and re-try" - ), - arguments={}, - ) - - optimization_level = options.get("optimization_level", 3) - _check_argument( - optimization_level > 0, - description="Q-CTRL Primitives do not support optimization level 0. Please\ - set optimization_level to 3 and re-try", - arguments={}, - ) - - -def _warn_and_clean_options(options: Dict[str, Any]) -> None: - """ - Validate and update transpilation settings - """ - # Issue a warning and override if any of these setting is not None - # or a different value than the default below - expected_options = { - "optimization_level": 3, - "resilience_level": 1, - "transpilation": {"approximation_degree": 0, "skip_transpilation": False}, - "resilience": { - "noise_amplifier": None, - "noise_factors": None, - "extrapolator": None, - }, - } - - # Collect keys with mis-matching values - different_keys = _validate_values(expected_options, options) - # Override options - _update_values(expected_options, options) - if different_keys: - logger.warning( - "The following settings cannot be customized and will be overwritten: %s", - ",".join(sorted(different_keys)), - ) - - def validate_v2(options: Dict[str, Any]) -> None: """Validates the options for qctrl""" @@ -104,13 +30,6 @@ def validate_v2(options: Dict[str, Any]) -> None: _warn_and_clean_options_v2(options) # Default validation otherwise. - TranspilationOptions(**options.get("transpilation", {})) - execution_time = options.get("max_execution_time") - if execution_time is not None and not isinstance(execution_time, UnsetType): - if execution_time > Options._MAX_EXECUTION_TIME: - raise ValueError( - f"max_execution_time must be below " f"{Options._MAX_EXECUTION_TIME} seconds." - ) EnvironmentOptions(**options.get("environment", {})) # ExecutionOptions(**options.get("execution", {})) diff --git a/qiskit_ibm_runtime/utils/sampler_result_decoder.py b/qiskit_ibm_runtime/utils/sampler_result_decoder.py index fa68339d0..905b143a5 100644 --- a/qiskit_ibm_runtime/utils/sampler_result_decoder.py +++ b/qiskit_ibm_runtime/utils/sampler_result_decoder.py @@ -12,11 +12,8 @@ """Sampler result decoder.""" -from typing import Dict, Union -from math import sqrt +from typing import Dict -from qiskit.result import QuasiDistribution -from qiskit.primitives import SamplerResult from qiskit.primitives import PrimitiveResult from .result_decoder import ResultDecoder @@ -26,29 +23,10 @@ class SamplerResultDecoder(ResultDecoder): """Class used to decode sampler results.""" @classmethod - def decode(cls, raw_result: str) -> Union[SamplerResult, PrimitiveResult]: + def decode(cls, raw_result: str) -> PrimitiveResult: """Convert the result to SamplerResult.""" decoded: Dict = super().decode(raw_result) - if isinstance(decoded, PrimitiveResult): - return decoded + return decoded # TODO: Handle V2 result that is returned in dict format - - # V1 result - quasi_dists = [] - for quasi, meta in zip(decoded["quasi_dists"], decoded["metadata"]): - shots = meta.get("shots", float("inf")) - overhead = meta.get("readout_mitigation_overhead", 1.0) - - # M3 mitigation overhead is gamma^2 - # https://github.com/Qiskit-Partners/mthree/blob/423d7e83a12491c59c9f58af46b75891bc622949/mthree/mitigation.py#L457 - # - # QuasiDistribution stddev_upper_bound is gamma / sqrt(shots) - # https://github.com/Qiskit/qiskit-terra/blob/ff267b5de8b83aef86e2c9ac6c7f918f58500505/qiskit/result/mitigation/local_readout_mitigator.py#L288 - stddev = sqrt(overhead / shots) - quasi_dists.append(QuasiDistribution(quasi, shots=shots, stddev_upper_bound=stddev)) - return SamplerResult( - quasi_dists=quasi_dists, - metadata=decoded["metadata"], - ) diff --git a/release-notes/unreleased/1857.other.rst b/release-notes/unreleased/1857.other.rst new file mode 100644 index 000000000..4d5c60097 --- /dev/null +++ b/release-notes/unreleased/1857.other.rst @@ -0,0 +1,2 @@ +The V1 Primitives ``SamplerV1`` and ``EstimatorV1`` have been completely removed. Please use the +V2 Primitives instead https://docs.quantum.ibm.com/migration-guides/v2-primitives. \ No newline at end of file diff --git a/test/ibm_test_case.py b/test/ibm_test_case.py index 3f9ee4abd..2dfd373ca 100644 --- a/test/ibm_test_case.py +++ b/test/ibm_test_case.py @@ -22,9 +22,8 @@ from collections import defaultdict from typing import DefaultDict, Dict -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import QISKIT_IBM_RUNTIME_LOGGER_NAME -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, SamplerV2, Options +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 from .utils import setup_test_logging, bell from .decorators import IntegrationTestDependencies, integration_test_setup @@ -251,22 +250,14 @@ def _run_program( "max_execution_time": max_execution_time, } if pid == "sampler": - backend = service.backend(backend_name) - options = Options() - if log_level: - options.environment.log_level = log_level - if job_tags: - options.environment.job_tags = job_tags - if max_execution_time: - options.max_execution_time = max_execution_time - sampler = Sampler(backend=backend, options=options) - job = sampler.run(circuits or bell(), callback=callback) - elif pid == "samplerv2": backend = service.backend(backend_name) sampler = SamplerV2(backend=backend) - pm = generate_preset_pass_manager(backend=backend, optimization_level=1) - isa_qc = pm.run(bell()) - job = sampler.run([isa_qc]) + if job_tags: + sampler.options.environment.job_tags = job_tags + if circuits: + job = sampler.run([circuits]) + else: + job = sampler.run([bell()]) else: job = service._run( program_id=pid, diff --git a/test/integration/test_estimator.py b/test/integration/test_estimator.py deleted file mode 100644 index b09d1d24b..000000000 --- a/test/integration/test_estimator.py +++ /dev/null @@ -1,228 +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. - -"""Integration tests for Estimator primitive.""" - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import RealAmplitudes -from qiskit.primitives import Estimator as TerraEstimator -from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives import BaseEstimator, EstimatorResult -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.providers.exceptions import QiskitBackendNotFoundError - -from qiskit_ibm_runtime import Estimator, Session - -from ..decorators import run_integration_test -from ..ibm_test_case import IBMIntegrationTestCase -from ..utils import bell - - -class TestIntegrationEstimator(IBMIntegrationTestCase): - """Integration tests for Estimator primitive.""" - - def setUp(self) -> None: - super().setUp() - self.backend = "ibmq_qasm_simulator" - - @run_integration_test - def test_estimator_session(self, service): - """Verify if estimator primitive returns expected results""" - - 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)]) - backend = service.backend(self.backend) - pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) - - with Session(service, self.backend) as session: - estimator = Estimator(session=session) - self.assertIsInstance(estimator, BaseEstimator) - - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] - - circuits1 = pm.run([psi1]) - # calculate [ ] - job = estimator.run(circuits=circuits1, observables=[H1], parameter_values=[theta1]) - result1 = job.result() - self.assertIsInstance(result1, EstimatorResult) - self.assertEqual(len(result1.values), len(circuits1)) - self.assertEqual(len(result1.metadata), len(circuits1)) - - circuits2 = pm.run(circuits1 * 2) - # calculate [ , ] - job = estimator.run( - circuits=circuits2, observables=[H2, H3], parameter_values=[theta1] * 2 - ) - result2 = job.result() - self.assertIsInstance(result2, EstimatorResult) - self.assertEqual(len(result2.values), len(circuits2)) - self.assertEqual(len(result2.metadata), len(circuits2)) - - circuits3 = pm.run([psi2]) - # calculate [ ] - job = estimator.run(circuits=circuits3, observables=[H2], parameter_values=[theta2]) - result3 = job.result() - self.assertIsInstance(result3, EstimatorResult) - self.assertEqual(len(result3.values), len(circuits3)) - self.assertEqual(len(result3.metadata), len(circuits3)) - - # calculate [ , ] - job = estimator.run( - circuits=circuits2, - observables=[H1, H1], - parameter_values=[theta1, theta3], - ) - result4 = job.result() - self.assertIsInstance(result4, EstimatorResult) - self.assertEqual(len(result4.values), len(circuits2)) - self.assertEqual(len(result4.metadata), len(circuits2)) - - circuits5 = pm.run([psi1, psi2, psi1]) - # calculate [ , - # , - # ] - job = estimator.run( - circuits=circuits5, - observables=[H1, H2, H3], - parameter_values=[theta1, theta2, theta3], - ) - result5 = job.result() - self.assertIsInstance(result5, EstimatorResult) - self.assertEqual(len(result5.values), len(circuits5)) - self.assertEqual(len(result5.metadata), len(circuits5)) - - @run_integration_test - def test_estimator_callback(self, service): - """Test Estimator callback function.""" - - def _callback(job_id_, result_): - nonlocal ws_result - ws_result.append(result_) - nonlocal job_ids - job_ids.add(job_id_) - - ws_result = [] - job_ids = set() - - bell_circuit = bell() - obs = SparsePauliOp.from_list([("IZ", 1)]) - - with Session(service, self.backend) as session: - estimator = Estimator(session=session) - job = estimator.run( - circuits=[bell_circuit] * 60, observables=[obs] * 60, callback=_callback - ) - result = job.result() - self.assertIsInstance(ws_result[-1], dict) - ws_result_values = np.asarray(ws_result[-1]["values"]) - self.assertTrue((result.values == ws_result_values).all()) - self.assertEqual(len(job_ids), 1) - self.assertEqual(job.job_id(), job_ids.pop()) - - @run_integration_test - def test_estimator_coeffs(self, service): - """Verify estimator with same operator different coefficients.""" - - cir = QuantumCircuit(2) - cir.h(0) - cir.cx(0, 1) - cir.ry(Parameter("theta"), 0) - - theta_vec = np.linspace(-np.pi, np.pi, 15) - - ## OBSERVABLE - obs1 = SparsePauliOp(["ZZ", "ZX", "XZ", "XX"], [1, -1, +1, 1]) - obs2 = SparsePauliOp(["ZZ", "ZX", "XZ", "XX"], [1, +1, -1, 1]) - - ## TERRA ESTIMATOR - estimator = TerraEstimator() - - job1 = estimator.run( - circuits=[cir] * len(theta_vec), - observables=[obs1] * len(theta_vec), - parameter_values=[[v] for v in theta_vec], - ) - job2 = estimator.run( - circuits=[cir] * len(theta_vec), - observables=[obs2] * len(theta_vec), - parameter_values=[[v] for v in theta_vec], - ) - - chsh1_terra = job1.result() - chsh2_terra = job2.result() - - with Session(service=service, backend=self.backend) as session: - estimator = Estimator(session=session) - - job1 = estimator.run( - circuits=[cir] * len(theta_vec), - observables=[obs1] * len(theta_vec), - parameter_values=[[v] for v in theta_vec], - ) - job2 = estimator.run( - circuits=[cir] * len(theta_vec), - observables=[obs2] * len(theta_vec), - parameter_values=[[v] for v in theta_vec], - ) - - chsh1_runtime = job1.result() - chsh2_runtime = job2.result() - - np.testing.assert_allclose(chsh1_terra.values, chsh1_runtime.values, rtol=0.3) - np.testing.assert_allclose(chsh2_terra.values, chsh2_runtime.values, rtol=0.3) - - @run_integration_test - def test_estimator_no_session(self, service): - """Test estimator primitive without a session.""" - backend = service.backend(self.backend) - pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) - circ_count = 3 - - psi1 = RealAmplitudes(num_qubits=2, reps=2) - - # pylint: disable=invalid-name - H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - - estimator = Estimator(backend=backend) - self.assertIsInstance(estimator, BaseEstimator) - self.assertIsNone(estimator.session) - - theta = [0, 1, 1, 2, 3, 5] - circuits = [psi1] * circ_count - isa_circuits = pm.run(circuits) - # calculate [ ] - job = estimator.run( - circuits=isa_circuits, - observables=[H1] * circ_count, - parameter_values=[theta] * circ_count, - ) - result1 = job.result() - self.assertIsInstance(result1, EstimatorResult) - self.assertEqual(len(result1.values), len(isa_circuits)) - self.assertEqual(len(result1.metadata), len(isa_circuits)) - self.assertIsNone(job.session_id) - - @run_integration_test - def test_estimator_backend_str(self, service): - """Test v1 primitive with string as backend.""" - # pylint: disable=unused-argument - with self.assertRaisesRegex(QiskitBackendNotFoundError, "No backend matches"): - _ = Estimator(backend="fake_manila") diff --git a/test/integration/test_job.py b/test/integration/test_job.py index 48e5e46c7..5f4fc5c2d 100644 --- a/test/integration/test_job.py +++ b/test/integration/test_job.py @@ -14,22 +14,17 @@ import random import time -import unittest -from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE from qiskit_ibm_runtime.exceptions import ( - RuntimeJobFailureError, RuntimeInvalidStateError, RuntimeJobNotFound, ) + from ..ibm_test_case import IBMIntegrationJobTestCase from ..decorators import run_integration_test, production_only, quantum_only from ..serialization import ( - get_complex_types, - SerializableClassDecoder, SerializableClass, ) from ..utils import cancel_job_safe, wait_for_status, get_real_device, bell @@ -43,7 +38,7 @@ def test_run_program(self, service): """Test running a program.""" job = self._run_program(service) job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("DONE", job.status()) self.assertTrue(job.result()) @run_integration_test @@ -69,21 +64,6 @@ def test_run_program_log_level(self, service): if job.logs(): self.assertIn("Completed", job.logs()) - @run_integration_test - @quantum_only - def test_run_program_failed(self, service): - """Test a failed program execution.""" - job = self._run_program(service, program_id="circuit-runner", inputs={}) - job.wait_for_final_state() - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn( - API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id(), ""), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError) as err_cm: - job.result() - self.assertIn("KeyError", str(err_cm.exception)) - @run_integration_test @production_only def test_cancel_job_queued(self, service): @@ -93,25 +73,24 @@ def test_cancel_job_queued(self, service): pm = generate_preset_pass_manager(optimization_level=1, target=real_device.target) _ = self._run_program(service, circuits=pm.run([bell()] * 10), backend=real_device_name) job = self._run_program(service, circuits=pm.run([bell()] * 2), backend=real_device_name) - wait_for_status(job, JobStatus.QUEUED) + wait_for_status(job, "QUEUED") if not cancel_job_safe(job, self.log): return time.sleep(15) # Wait a bit for DB to update. rjob = service.job(job.job_id()) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) + self.assertEqual(rjob.status(), "CANCELLED") @run_integration_test def test_cancel_job_running(self, service): """Test canceling a running job.""" job = self._run_program( service, - circuits=[bell()] * 10, ) rjob = service.job(job.job_id()) if not cancel_job_safe(rjob, self.log): return time.sleep(5) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) + self.assertEqual(rjob.status(), "CANCELLED") @run_integration_test def test_cancel_job_done(self, service): @@ -124,7 +103,7 @@ def test_cancel_job_done(self, service): @run_integration_test def test_delete_job(self, service): """Test deleting a job.""" - sub_tests = [JobStatus.DONE] + sub_tests = ["DONE"] for status in sub_tests: with self.subTest(status=status): job = self._run_program(service) @@ -143,23 +122,11 @@ def test_delete_job_queued(self, service): isa_circuit = pm.run([bell()]) _ = self._run_program(service, circuits=isa_circuit, backend=real_device_name) job = self._run_program(service, circuits=isa_circuit, backend=real_device_name) - wait_for_status(job, JobStatus.QUEUED) + wait_for_status(job, "QUEUED") service.delete_job(job.job_id()) with self.assertRaises(RuntimeJobNotFound): service.job(job.job_id()) - @unittest.skip("skip until qiskit-ibm-runtime #933 is fixed") - @run_integration_test - def test_final_result(self, service): - """Test getting final result.""" - final_result = get_complex_types() - job = self._run_program(service) - result = job.result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, result) - - rresults = service.job(job.job_id()).result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, rresults) - @run_integration_test def test_job_status(self, service): """Test job status.""" @@ -167,21 +134,6 @@ def test_job_status(self, service): time.sleep(random.randint(1, 5)) self.assertTrue(job.status()) - @run_integration_test - @quantum_only - def test_job_inputs(self, service): - """Test job inputs.""" - interim_results = get_complex_types() - inputs = { - "interim_results": interim_results, - "circuits": bell(), - } - job = self._run_program(service, inputs=inputs, program_id="circuit-runner") - self.assertEqual(inputs, job.inputs) - rjob = service.job(job.job_id()) - rinterim_results = rjob.inputs["interim_results"] - self._assert_complex_types_equal(interim_results, rinterim_results) - @run_integration_test def test_job_backend(self, service): """Test job backend.""" @@ -199,7 +151,7 @@ def test_wait_for_final_state(self, service): """Test wait for final state.""" job = self._run_program(service, backend="ibmq_qasm_simulator") job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("DONE", job.status()) @run_integration_test @production_only @@ -217,10 +169,10 @@ def test_wait_for_final_state_after_job_status(self, service): """Test wait for final state on a completed job when the status is updated first.""" job = self._run_program(service, backend="ibmq_qasm_simulator") status = job.status() - while status not in JOB_FINAL_STATES: + while status not in ["DONE", "CANCELLED", "ERROR"]: status = job.status() job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("DONE", job.status()) @run_integration_test def test_job_creation_date(self, service): @@ -233,17 +185,6 @@ def test_job_creation_date(self, service): for rjob in rjobs: self.assertTrue(rjob.creation_date) - @unittest.skip("Skipping until primitives add more logging") - @run_integration_test - def test_job_logs(self, service): - """Test job logs.""" - job = self._run_program(service) - with self.assertLogs("qiskit_ibm_runtime", "INFO"): - job.logs() - job.wait_for_final_state() - time.sleep(1) - self.assertTrue(job.logs()) - @run_integration_test def test_job_metrics(self, service): """Test job metrics.""" diff --git a/test/integration/test_options.py b/test/integration/test_options.py deleted file mode 100644 index d270a0a93..000000000 --- a/test/integration/test_options.py +++ /dev/null @@ -1,169 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for job functions using real runtime service.""" - -from qiskit import QuantumCircuit - -from qiskit.circuit.library import RealAmplitudes -from qiskit.quantum_info import SparsePauliOp -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_aer.noise import NoiseModel -from qiskit_ibm_runtime import Session, Sampler, Options, Estimator -from qiskit_ibm_runtime.fake_provider import FakeManila -from qiskit_ibm_runtime.exceptions import RuntimeJobFailureError - -from ..ibm_test_case import IBMIntegrationTestCase -from ..decorators import run_integration_test, production_only - - -class TestIntegrationOptions(IBMIntegrationTestCase): - """Integration tests for options.""" - - @run_integration_test - def test_noise_model(self, service): - """Test running with noise model.""" - backend = service.backend("ibmq_qasm_simulator") - self.log.info("Using backend %s", backend.name) - - fake_backend = FakeManila() - noise_model = NoiseModel.from_backend(fake_backend) - - circ = QuantumCircuit(1, 1) - circ.x(0) - circ.measure_all(add_bits=False) - - options = Options( - simulator={ - "noise_model": noise_model, - "basis_gates": fake_backend.configuration().basis_gates, - "coupling_map": fake_backend.configuration().coupling_map, - "seed_simulator": 42, - }, - resilience_level=0, - ) - - with Session(service=service, backend=backend): - sampler = Sampler(options=options) - job1 = sampler.run(circ) - self.log.info("Runtime job %s submitted.", job1.job_id()) - result1 = job1.result() - # We should get both 0 and 1 if there is noise. - self.assertEqual(len(result1.quasi_dists[0].keys()), 2) - - job2 = sampler.run(circ) - self.log.info("Runtime job %s submitted.", job2.job_id()) - result2 = job2.result() - # We should get both 0 and 1 if there is noise. - self.assertEqual(len(result2.quasi_dists[0].keys()), 2) - # The results should be the same because we used the same seed. - self.assertEqual(result1.quasi_dists, result2.quasi_dists) - - @run_integration_test - def test_simulator_transpile(self, service): - """Test simulator transpile options.""" - backend = service.backend("ibmq_qasm_simulator") - self.log.info("Using backend %s", backend.name) - - circ = QuantumCircuit(2, 2) - circ.cx(0, 1) - circ.measure_all(add_bits=False) - obs = SparsePauliOp.from_list([("IZ", 1)]) - - option_vars = [ - Options(simulator={"coupling_map": []}), - Options(simulator={"basis_gates": ["foo"]}), - ] - - with Session(service=service, backend=backend): - for opt in option_vars: - with self.subTest(opt=opt): - sampler = Sampler(options=opt) - job1 = sampler.run(circ) - self.log.info("Runtime job %s submitted.", job1.job_id()) - with self.assertRaises(RuntimeJobFailureError): - job1.result() - # TODO: Re-enable when ntc-1651 is fixed - # self.assertIn("TranspilerError", err.exception.message) - - estimator = Estimator(options=opt) - job2 = estimator.run(circ, observables=obs) - with self.assertRaises(RuntimeJobFailureError): - job2.result() - # TODO: Re-enable when ntc-1651 is fixed - # self.assertIn("TranspilerError", err.exception.message) - - @run_integration_test - def test_unsupported_input_combinations(self, service): - """Test that when resilience_level==3, and backend is a simulator, - a coupling map is required.""" - circ = QuantumCircuit(1) - obs = SparsePauliOp.from_list([("I", 1)]) - options = Options() - options.resilience_level = 3 - backend = service.backend("ibmq_qasm_simulator") - with Session(service=service, backend=backend) as session: - with self.assertRaises(ValueError) as exc: - inst = Estimator(session=session, options=options) - inst.run(circ, observables=obs) - self.assertIn("a coupling map is required.", str(exc.exception)) - - @run_integration_test - def test_default_resilience_settings(self, service): - """Test that correct default resilience settings are used.""" - circ = QuantumCircuit(1) - obs = SparsePauliOp.from_list([("I", 1)]) - options = Options(resilience_level=2) - backend = service.backend("ibmq_qasm_simulator") - with Session(service=service, backend=backend) as session: - inst = Estimator(session=session, options=options) - job = inst.run(circ, observables=obs) - self.assertEqual(job.inputs["resilience_settings"]["noise_factors"], [1, 3, 5]) - self.assertEqual( - job.inputs["resilience_settings"]["extrapolator"], "LinearExtrapolator" - ) - - options = Options(resilience_level=1) - with Session(service=service, backend=backend) as session: - inst = Estimator(session=session, options=options) - job = inst.run(circ, observables=obs) - self.assertNotIn("noise_factors", job.inputs["resilience_settings"]) - self.assertNotIn("extrapolator", job.inputs["resilience_settings"]) - - @production_only - @run_integration_test - def test_all_resilience_levels(self, service): - """Test that all resilience_levels are recognized correctly - by checking their values in the metadata""" - resilience_values = { - 0: "variance", - 1: "readout_mitigation_num_twirled_circuits", - 2: "zne", - 3: "standard_error", - } - psi1 = RealAmplitudes(num_qubits=2, reps=2) - h_1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - - backend = service.backend("ibmq_qasm_simulator") - pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) - options = Options() - options.simulator.coupling_map = [[0, 1], [1, 0]] - - for level, value in resilience_values.items(): - options.resilience_level = level - inst = Estimator(backend=backend, options=options) - theta1 = [0, 1, 1, 2, 3, 5] - result = inst.run( - circuits=pm.run([psi1]), observables=[h_1], parameter_values=[theta1] - ).result() - metadata = result.metadata[0] - self.assertTrue(value in metadata) diff --git a/test/integration/test_retrieve_job.py b/test/integration/test_retrieve_job.py index 49e849062..cc3ab2638 100644 --- a/test/integration/test_retrieve_job.py +++ b/test/integration/test_retrieve_job.py @@ -16,7 +16,6 @@ from datetime import datetime, timezone from qiskit.providers.jobstatus import JobStatus from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import RuntimeJob, RuntimeJobV2 from ..ibm_test_case import IBMIntegrationJobTestCase @@ -68,7 +67,7 @@ def test_retrieve_all_jobs(self, service): for rjob in rjobs: if rjob.job_id() == job.job_id(): self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.result(), rjob.result()) + self.assertEqual(job.status(), rjob.status()) found = True break self.assertTrue(found, f"Job {job.job_id()} not returned.") @@ -89,8 +88,7 @@ def test_retrieve_jobs_limit(self, service): @run_integration_test def test_retrieve_pending_jobs(self, service): """Test retrieving pending jobs (QUEUED, RUNNING).""" - circuits = [bell()] * 20 - job = self._run_program(service, circuits=circuits) + job = self._run_program(service) wait_for_status(job, JobStatus.RUNNING) rjobs = service.jobs(pending=True) after_status = job.status() @@ -98,7 +96,7 @@ def test_retrieve_pending_jobs(self, service): for rjob in rjobs: if rjob.job_id() == job.job_id(): self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs["run_options"], rjob.inputs["run_options"]) + self.assertEqual(job.inputs, rjob.inputs) found = True break @@ -117,7 +115,7 @@ def test_retrieve_returned_jobs(self, service): for rjob in rjobs: if rjob.job_id() == job.job_id(): self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.result(), rjob.result()) + self.assertEqual(job.status(), rjob.status()) found = True break self.assertTrue(found, f"Returned job {job.job_id()} not retrieved.") @@ -203,15 +201,3 @@ def test_retrieve_jobs_backend(self, service): jobs = service.jobs(backend_name=backend) for job in jobs: self.assertEqual(backend, job.backend().name) - - @run_integration_test - def test_retrieve_correct_job_version(self, service): - """Test retrieving the correct job version.""" - job = self._run_program(service) - job.wait_for_final_state() - rjob = service.job(job.job_id()) - job_v2 = self._run_program(service, program_id="samplerv2") - job_v2.wait_for_final_state() - rjob_v2 = service.job(job_v2.job_id()) - self.assertIsInstance(rjob, RuntimeJob) - self.assertIsInstance(rjob_v2, RuntimeJobV2) diff --git a/test/integration/test_sampler.py b/test/integration/test_sampler.py deleted file mode 100644 index 12e03748b..000000000 --- a/test/integration/test_sampler.py +++ /dev/null @@ -1,182 +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. - -"""Integration tests for Sampler primitive.""" - -from math import sqrt - -from qiskit.circuit import QuantumCircuit, Gate -from qiskit.circuit.library import RealAmplitudes - -from qiskit.primitives import BaseSampler, SamplerResult -from qiskit.result import QuasiDistribution -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.providers.exceptions import QiskitBackendNotFoundError - -from qiskit_ibm_runtime import Sampler, Session -from qiskit_ibm_runtime.exceptions import RuntimeJobFailureError - -from ..decorators import run_integration_test -from ..ibm_test_case import IBMIntegrationTestCase -from ..utils import bell - - -class TestIntegrationIBMSampler(IBMIntegrationTestCase): - """Integration tests for Sampler primitive.""" - - def setUp(self) -> None: - super().setUp() - self.bell = bell() - self.backend = "ibmq_qasm_simulator" - - @run_integration_test - def test_sampler_non_parameterized_circuits(self, service): - """Test sampler with multiple non-parameterized circuits.""" - # Execute three Bell circuits - with Session(service, self.backend) as session: - sampler = Sampler(session=session) - self.assertIsInstance(sampler, BaseSampler) - circuits = [self.bell] * 3 - - circuits1 = circuits - result1 = sampler.run(circuits=circuits1).result() - self.assertIsInstance(result1, SamplerResult) - self.assertEqual(len(result1.quasi_dists), len(circuits1)) - self.assertEqual(len(result1.metadata), len(circuits1)) - for i in range(len(circuits1)): - self.assertAlmostEqual(result1.quasi_dists[i][3], 0.5, delta=0.1) - self.assertAlmostEqual(result1.quasi_dists[i][0], 0.5, delta=0.1) - - circuits2 = [circuits[0], circuits[2]] - result2 = sampler.run(circuits=circuits2).result() - self.assertIsInstance(result2, SamplerResult) - self.assertEqual(len(result2.quasi_dists), len(circuits2)) - self.assertEqual(len(result2.metadata), len(circuits2)) - for i in range(len(circuits2)): - self.assertAlmostEqual(result2.quasi_dists[i][3], 0.5, delta=0.1) - self.assertAlmostEqual(result2.quasi_dists[i][0], 0.5, delta=0.1) - - circuits3 = [circuits[1], circuits[2]] - result3 = sampler.run(circuits=circuits3).result() - self.assertIsInstance(result3, SamplerResult) - self.assertEqual(len(result3.quasi_dists), len(circuits3)) - self.assertEqual(len(result3.metadata), len(circuits3)) - for i in range(len(circuits3)): - self.assertAlmostEqual(result3.quasi_dists[i][3], 0.5, delta=0.1) - self.assertAlmostEqual(result3.quasi_dists[i][0], 0.5, delta=0.1) - - @run_integration_test - def test_sampler_primitive_parameterized_circuits(self, service): - """Verify if sampler primitive returns expected results for parameterized circuits.""" - - # parameterized circuit - 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 = [1, 2, 3, 4, 5, 6] - theta3 = [0, 1, 2, 3, 4, 5, 6, 7] - backend = service.backend(self.backend) - pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) - - with Session(service, self.backend) as session: - sampler = Sampler(session=session) - self.assertIsInstance(sampler, BaseSampler) - - circuits0 = pm.run([pqc, pqc, pqc2]) - result = sampler.run( - circuits=circuits0, - parameter_values=[theta1, theta2, theta3], - ).result() - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), len(circuits0)) - self.assertEqual(len(result.metadata), len(circuits0)) - - @run_integration_test - def test_sampler_skip_transpile(self, service): - """Test skip transpilation option.""" - circ = QuantumCircuit(1, 1) - custom_gate = Gate("my_custom_gate", 1, [3.14, 1]) - circ.append(custom_gate, [0]) - circ.measure(0, 0) - - with Session(service, self.backend) as session: - sampler = Sampler(session=session) - with self.assertRaises(RuntimeJobFailureError) as err: - sampler.run(circuits=circ, skip_transpilation=True).result() - # If transpilation not skipped the error would be something about cannot expand. - self.assertIn("invalid instructions", err.exception.message) - - @run_integration_test - def test_sampler_optimization_level(self, service): - """Test transpiler optimization level is properly mapped.""" - with Session(service, self.backend) as session: - sampler = Sampler(session=session, options={"optimization_level": 1}) - shots = 1000 - result = sampler.run(self.bell, shots=shots).result() - self.assertEqual(result.quasi_dists[0].shots, shots) - self.assertAlmostEqual( - result.quasi_dists[0]._stddev_upper_bound, sqrt(1 / shots), delta=0.1 - ) - self.assertAlmostEqual(result.quasi_dists[0][3], 0.5, delta=0.1) - self.assertAlmostEqual(result.quasi_dists[0][0], 0.5, delta=0.1) - - @run_integration_test - def test_sampler_callback(self, service): - """Test Sampler callback function.""" - - def _callback(job_id_, result_): - nonlocal ws_result - ws_result.append(result_) - nonlocal job_ids - job_ids.add(job_id_) - - ws_result = [] - job_ids = set() - - with Session(service, self.backend) as session: - sampler = Sampler(session=session) - job = sampler.run(circuits=[self.bell] * 20, callback=_callback) - result = job.result() - - self.assertIsInstance(ws_result[-1], dict) - ws_result_quasi = [QuasiDistribution(quasi) for quasi in ws_result[-1]["quasi_dists"]] - self.assertEqual(result.quasi_dists, ws_result_quasi) - self.assertEqual(len(job_ids), 1) - self.assertEqual(job.job_id(), job_ids.pop()) - - @run_integration_test - def test_sampler_no_session(self, service): - """Test sampler without session.""" - backend = service.backend(self.backend) - sampler = Sampler(backend=backend) - self.assertIsInstance(sampler, BaseSampler) - - circuits = [self.bell] * 3 - job = sampler.run(circuits=circuits) - result = job.result() - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), len(circuits)) - self.assertEqual(len(result.metadata), len(circuits)) - for i in range(len(circuits)): - self.assertAlmostEqual(result.quasi_dists[i][3], 0.5, delta=0.1) - self.assertAlmostEqual(result.quasi_dists[i][0], 0.5, delta=0.1) - self.assertIsNone(job.session_id) - - @run_integration_test - def test_sampler_backend_str(self, service): - """Test v1 primitive with string as backend.""" - # pylint: disable=unused-argument - with self.assertRaisesRegex(QiskitBackendNotFoundError, "No backend matches"): - _ = Sampler(backend="fake_manila") diff --git a/test/integration/test_session.py b/test/integration/test_session.py index 5a8cb1746..68729a2cc 100644 --- a/test/integration/test_session.py +++ b/test/integration/test_session.py @@ -20,7 +20,7 @@ from qiskit.primitives import EstimatorResult, SamplerResult from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import Estimator, Session, Sampler, Options, Batch, SamplerV2 +from qiskit_ibm_runtime import Session, Batch, SamplerV2, EstimatorV2 from qiskit_ibm_runtime.exceptions import IBMInputValueError from ..utils import bell @@ -40,44 +40,26 @@ def test_estimator_sampler(self, service): H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) theta1 = [0, 1, 1, 2, 3, 5] - options = Options(resilience_level=0) backend = service.backend("ibmq_qasm_simulator") pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) with Session(service, backend=backend) as session: - estimator = Estimator(session=session, options=options) - result = estimator.run( - circuits=pm.run([psi1]), observables=[H1], parameter_values=[theta1], shots=100 - ).result() + estimator = EstimatorV2(session=session) + result = estimator.run([(psi1, H1, [theta1])]).result() self.assertIsInstance(result, EstimatorResult) - self.assertEqual(len(result.values), 1) - self.assertEqual(len(result.metadata), 1) - self.assertEqual(result.metadata[0]["shots"], 100) - sampler = Sampler(session=session, options=options) - result = sampler.run(circuits=pm.run(bell()), shots=200).result() + sampler = SamplerV2(session=session) + result = sampler.run([pm.run(bell())]).result() self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 1) - self.assertEqual(len(result.metadata), 1) - self.assertEqual(result.metadata[0]["shots"], 200) - self.assertAlmostEqual(result.quasi_dists[0][3], 0.5, delta=0.1) - self.assertAlmostEqual(result.quasi_dists[0][0], 0.5, delta=0.1) - result = estimator.run( - circuits=pm.run([psi1]), observables=[H1], parameter_values=[theta1], shots=300 - ).result() + result = estimator.run([(psi1, H1, [theta1])]).result() self.assertIsInstance(result, EstimatorResult) self.assertEqual(len(result.values), 1) self.assertEqual(len(result.metadata), 1) self.assertEqual(result.metadata[0]["shots"], 300) - result = sampler.run(circuits=pm.run(bell()), shots=400).result() + result = sampler.run([pm.run(bell())]).result() self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 1) - self.assertEqual(len(result.metadata), 1) - self.assertEqual(result.metadata[0]["shots"], 400) - self.assertAlmostEqual(result.quasi_dists[0][3], 0.5, delta=0.1) - self.assertAlmostEqual(result.quasi_dists[0][0], 0.5, delta=0.1) session.close() @run_integration_test @@ -86,9 +68,10 @@ def test_using_correct_instance(self, service): """Test the instance used when filtering backends is honored.""" instance = self.dependencies.instance backend = service.backend("ibmq_qasm_simulator", instance=instance) + pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) with Session(service, backend=backend) as session: - sampler = Sampler(session=session) - job = sampler.run(bell(), shots=400) + sampler = SamplerV2(session=session) + job = sampler.run([pm.run(bell())]) self.assertEqual(instance, backend._instance) self.assertEqual(instance, job.backend()._instance) @@ -102,7 +85,7 @@ def test_session_from_id(self, service): pm = generate_preset_pass_manager(backend=backend, optimization_level=1) isa_circuit = pm.run(bell()) with Session(service, backend=backend) as session: - sampler = Sampler(session=session) + sampler = SamplerV2(session=session) sampler.run(isa_circuit) new_session = Session.from_id(session_id=session._session_id, service=service) diff --git a/test/qctrl/test_qctrl.py b/test/qctrl/test_qctrl.py index ef46bb8ce..c56414247 100644 --- a/test/qctrl/test_qctrl.py +++ b/test/qctrl/test_qctrl.py @@ -12,28 +12,21 @@ """Tests for job functions using real runtime service.""" -from qiskit import QuantumCircuit -from qiskit.quantum_info import Statevector, hellinger_fidelity -from qiskit.providers.jobstatus import JobStatus from qiskit.quantum_info import SparsePauliOp from qiskit.circuit.library import RealAmplitudes from qiskit.primitives.containers import PrimitiveResult, PubResult, DataBin, BitArray from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import ( - Sampler, SamplerV2, EstimatorV2, Batch, - Options, - Estimator, - QiskitRuntimeService, ) -from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError + from ..ibm_test_case import IBMIntegrationTestCase from ..decorators import run_integration_test -from ..utils import cancel_job_safe, bell +from ..utils import bell FIDELITY_THRESHOLD = 0.8 DIFFERENCE_THRESHOLD = 0.35 @@ -118,366 +111,3 @@ def _verify_estimator_result(self, result, num_pubs, shapes): self.assertTrue(pub_result.metadata) self.assertEqual(pub_result.data.evs.shape, shapes[idx]) self.assertEqual(pub_result.data.stds.shape, shapes[idx]) - - -class TestQCTRL(IBMIntegrationTestCase): - """Integration tests for QCTRL integration.""" - - def setUp(self) -> None: - super().setUp() - self.bell = bell() - self.backend = "alt_canberra" - - def test_channel_strategy_parameter(self): - """Test passing in channel strategy parameter for a q-ctrl instance.""" - service = QiskitRuntimeService( - channel="ibm_cloud", - url=self.dependencies.url, - token=self.dependencies.token, - instance=self.dependencies.instance, - channel_strategy="q-ctrl", - ) - self.assertTrue(service) - - def test_invalid_channel_strategy_parameter(self): - """Test passing in invalid channel strategy parameter for a q-ctrl instance.""" - with self.assertRaises(IBMNotAuthorizedError): - QiskitRuntimeService( - channel="ibm_cloud", - url=self.dependencies.url, - token=self.dependencies.token, - instance=self.dependencies.instance, - channel_strategy=None, - ) - - @run_integration_test - def test_cancel_qctrl_job(self, service): - """Test canceling qctrl job.""" - with Batch(service, self.backend): - options = Options(resilience_level=1) - sampler = Sampler(options=options) - - job = sampler.run([self.bell] * 10) - - rjob = service.job(job.job_id()) - if not cancel_job_safe(rjob, self.log): - return - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - @run_integration_test - def test_sampler_qctrl_bell(self, service): - """Test qctrl bell state""" - # Set shots for experiment - shots = 1000 - - # Create Bell test circuit - bell_circuit = QuantumCircuit(2) - bell_circuit.h(0) - bell_circuit.cx(0, 1) - - # Add measurements for the sampler - bell_circuit_sampler = bell_circuit.copy() - bell_circuit_sampler.measure_active() - - # Execute circuit in a session with sampler - with Batch(service, backend=self.backend): - options = Options(resilience_level=1) - sampler = Sampler(options=options) - - result = sampler.run(bell_circuit_sampler, shots=shots).result() - results_dict = { - "{0:02b}".format(key): value for key, value in result.quasi_dists[0].items() - } # convert keys to bitstrings - - ideal_result = { - key: val / shots for key, val in Statevector(bell_circuit).probabilities_dict().items() - } - fidelity = hellinger_fidelity(results_dict, ideal_result) - - self.assertGreater(fidelity, FIDELITY_THRESHOLD) - - @run_integration_test - def test_sampler_qctrl_ghz(self, service): - """Test qctrl small GHZ""" - shots = 1000 - num_qubits = 5 - ghz_circuit = QuantumCircuit(num_qubits) - ghz_circuit.h(0) - for i in range(num_qubits - 1): - ghz_circuit.cx(i, i + 1) - - # Add measurements for the sampler - ghz_circuit_sampler = ghz_circuit.copy() - ghz_circuit_sampler.measure_active() - - # Execute circuit in a session with sampler - with Batch(service, backend=self.backend): - options = Options(resilience_level=1) - sampler = Sampler(options=options) - - result = sampler.run(ghz_circuit_sampler, shots=shots).result() - results_dict = { - f"{{0:0{num_qubits}b}}".format(key): value - for key, value in result.quasi_dists[0].items() - } # convert keys to bitstrings - - ideal_result = { - key: val / shots for key, val in Statevector(ghz_circuit).probabilities_dict().items() - } - fidelity = hellinger_fidelity(results_dict, ideal_result) - self.assertGreater(fidelity, FIDELITY_THRESHOLD) - - @run_integration_test - def test_sampler_qctrl_superposition(self, service): - """Test qctrl small superposition""" - - shots = 1000 - num_qubits = 5 - superposition_circuit = QuantumCircuit(num_qubits) - superposition_circuit.h(range(num_qubits)) - - # Add measurements for the sampler - superposition_circuit_sampler = superposition_circuit.copy() - superposition_circuit_sampler.measure_active() - - # Execute circuit in a session with sampler - with Batch(service, backend=self.backend): - options = Options(resilience_level=1) - sampler = Sampler(options=options) - - result = sampler.run(superposition_circuit_sampler, shots=shots).result() - results_dict = { - f"{{0:0{num_qubits}b}}".format(key): value - for key, value in result.quasi_dists[0].items() - } # convert keys to bitstrings - - ideal_result = { - key: val / shots - for key, val in Statevector(superposition_circuit).probabilities_dict().items() - } - fidelity = hellinger_fidelity(results_dict, ideal_result) - self.assertGreater(fidelity, FIDELITY_THRESHOLD) - - @run_integration_test - def test_sampler_qctrl_computational_states(self, service): - """Test qctrl computational states""" - shots = 1000 - num_qubits = 3 - computational_states_circuits = [] - for idx in range(2**num_qubits): - circuit = QuantumCircuit(num_qubits) - bitstring = f"{{0:0{num_qubits}b}}".format(idx) - for bit_pos, bit in enumerate( - bitstring[::-1] - ): # convert to little-endian (qiskit convention) - if bit == "1": - circuit.x(bit_pos) - computational_states_circuits.append(circuit) - - # Add measurements for the sampler - computational_states_sampler_circuits = [] - for circuit in computational_states_circuits: - circuit_sampler = circuit.copy() - circuit_sampler.measure_all() - computational_states_sampler_circuits.append(circuit_sampler) - - # Execute circuit in a session with sampler - with Batch(service, backend=self.backend): - options = Options(resilience_level=1) - sampler = Sampler(options=options) - - result = sampler.run(computational_states_sampler_circuits, shots=shots).result() - results_dict_list = [ - {f"{{0:0{num_qubits}b}}".format(key): value for key, value in quasis.items()} - for quasis in result.quasi_dists - ] # convert keys to bitstrings - - ideal_results_list = [ - {key: val / shots for key, val in Statevector(circuit).probabilities_dict().items()} - for circuit in computational_states_circuits - ] - fidelities = [ - hellinger_fidelity(results_dict, ideal_result) - for results_dict, ideal_result in zip(results_dict_list, ideal_results_list) - ] - - for fidelity in fidelities: - self.assertGreater(fidelity, FIDELITY_THRESHOLD) - - @run_integration_test - def test_estimator_qctrl_bell(self, service): - """Test estimator qctrl bell state""" - # Set shots for experiment - shots = 1000 - - # Create Bell test circuit - bell_circuit = QuantumCircuit(2) - bell_circuit.h(0) - bell_circuit.cx(0, 1) - - # Measure some observables in the estimator - observables = [SparsePauliOp("ZZ"), SparsePauliOp("IZ"), SparsePauliOp("ZI")] - - # Execute circuit in a session with estimator - with Batch(service, backend=self.backend): - estimator = Estimator() - - result = estimator.run( - [bell_circuit] * len(observables), observables=observables, shots=shots - ).result() - - ideal_result = [ - Statevector(bell_circuit).expectation_value(observable).real - for observable in observables - ] - absolute_difference = [ - abs(obs_theory - obs_exp) for obs_theory, obs_exp in zip(ideal_result, result.values) - ] - # absolute_difference_dict = { - # obs.paulis[0].to_label(): diff for obs, diff in zip(observables, absolute_difference) - # } - - for diff in absolute_difference: - self.assertLess(diff, DIFFERENCE_THRESHOLD) - - @run_integration_test - def test_estimator_qctrl_ghz(self, service): - """Test estimator qctrl GHZ state""" - shots = 1000 - num_qubits = 5 - ghz_circuit = QuantumCircuit(num_qubits) - ghz_circuit.h(0) - for i in range(num_qubits - 1): - ghz_circuit.cx(i, i + 1) - - # Measure some observables in the estimator - observables = [ - SparsePauliOp("Z" * num_qubits), - SparsePauliOp("I" * (num_qubits - 1) + "Z"), - SparsePauliOp("Z" + "I" * (num_qubits - 1)), - ] - - # Execute circuit in a session with estimator - with Batch(service, backend=self.backend): - estimator = Estimator() - - result = estimator.run( - [ghz_circuit] * len(observables), observables=observables, shots=shots - ).result() - - ideal_result = [ - Statevector(ghz_circuit).expectation_value(observable).real - for observable in observables - ] - absolute_difference = [ - abs(obs_theory - obs_exp) for obs_theory, obs_exp in zip(ideal_result, result.values) - ] - absolute_difference_dict = { - obs.paulis[0].to_label(): diff for obs, diff in zip(observables, absolute_difference) - } - - print( - "absolute difference between theory and experiment expectation values: ", - absolute_difference_dict, - ) - for diff in absolute_difference: - self.assertLess(diff, DIFFERENCE_THRESHOLD) - - @run_integration_test - def test_estimator_qctrl_superposition(self, service): - """Test estimator qctrl small superposition""" - shots = 1000 - num_qubits = 4 - superposition_circuit = QuantumCircuit(num_qubits) - superposition_circuit.h(range(num_qubits)) - - # Measure some observables in the estimator - obs_labels = [["I"] * num_qubits for _ in range(num_qubits)] - for idx, obs in enumerate(obs_labels): - obs[idx] = "Z" - obs_labels = ["".join(obs) for obs in obs_labels] - observables = [SparsePauliOp(obs) for obs in obs_labels] - - # Execute circuit in a session with estimator - with Batch(service, backend=self.backend): - estimator = Estimator() - - result = estimator.run( - [superposition_circuit] * len(observables), observables=observables, shots=shots - ).result() - - ideal_result = [ - Statevector(superposition_circuit).expectation_value(observable).real - for observable in observables - ] - absolute_difference = [ - abs(obs_theory - obs_exp) for obs_theory, obs_exp in zip(ideal_result, result.values) - ] - # absolute_difference_dict = { - # obs.paulis[0].to_label(): diff for obs, diff in zip(observables, absolute_difference) - # } - - for diff in absolute_difference: - self.assertLess(diff, DIFFERENCE_THRESHOLD) - - @run_integration_test - def test_estimator_qctrl_computational(self, service): - """Test estimator qctrl computational states""" - shots = 1000 - num_qubits = 3 - computational_states_circuits = [] - for idx in range(2**num_qubits): - circuit = QuantumCircuit(num_qubits) - bitstring = f"{{0:0{num_qubits}b}}".format(idx) - for bit_pos, bit in enumerate( - bitstring[::-1] - ): # convert to little-endian (qiskit convention) - if bit == "1": - circuit.x(bit_pos) - computational_states_circuits.append(circuit) - - # Measure some observables in the estimator - obs_labels = [["I"] * num_qubits for _ in range(num_qubits)] - for idx, obs in enumerate(obs_labels): - obs[idx] = "Z" - obs_labels = ["".join(obs) for obs in obs_labels] - observables = [SparsePauliOp(obs) for obs in obs_labels] - - computational_states_circuits_estimator, observables_estimator = [], [] - for circuit in computational_states_circuits: - computational_states_circuits_estimator += [circuit] * len(observables) - observables_estimator += observables - - # Execute circuit in a session with estimator - with Batch(service, self.backend): - estimator = Estimator() - result = estimator.run( - computational_states_circuits_estimator, - observables=observables_estimator, - shots=shots, - ).result() - - ideal_result = [ - Statevector(circuit).expectation_value(observable).real - for circuit, observable in zip( - computational_states_circuits_estimator, observables_estimator - ) - ] - absolute_difference = [ - abs(obs_theory - obs_exp) for obs_theory, obs_exp in zip(ideal_result, result.values) - ] - - absolute_difference_dict = {} - for idx in range(2**num_qubits): - circuit = QuantumCircuit(num_qubits) - bitstring = f"{{0:0{num_qubits}b}}".format(idx) - - absolute_difference_dict[bitstring] = { - obs.paulis[0].to_label(): diff - for obs, diff in zip( - observables_estimator[idx * len(observables) : (idx + 1) * len(observables)], - absolute_difference[idx * len(observables) : (idx + 1) * len(observables)], - ) - } - for diff in absolute_difference: - self.assertLess(diff, DIFFERENCE_THRESHOLD) diff --git a/test/unit/test_estimator.py b/test/unit/test_estimator.py index 723507f4c..43e72d6b8 100644 --- a/test/unit/test_estimator.py +++ b/test/unit/test_estimator.py @@ -20,7 +20,7 @@ from qiskit.quantum_info import SparsePauliOp, Pauli from qiskit.primitives.containers.estimator_pub import EstimatorPub -from qiskit_ibm_runtime import Estimator, Session, EstimatorV2, EstimatorOptions, IBMInputValueError +from qiskit_ibm_runtime import Session, EstimatorV2, EstimatorOptions, IBMInputValueError from qiskit_ibm_runtime.fake_provider import FakeSherbrooke from .mock.fake_runtime_service import FakeRuntimeService @@ -35,32 +35,6 @@ ) -class TestEstimator(IBMTestCase): - """Class for testing the Estimator class.""" - - def setUp(self) -> None: - super().setUp() - self.circuit = QuantumCircuit(1, 1) - self.observables = SparsePauliOp.from_list([("I", 1)]) - - def test_unsupported_values_for_estimator_options(self): - """Test exception when options levels are not supported.""" - options_bad = [ - {"resilience_level": 4, "optimization_level": 1}, - {"optimization_level": 4, "resilience_level": 2}, - ] - - with Session( - service=FakeRuntimeService(channel="ibm_quantum", token="abc"), - backend="common_backend", - ) as session: - for bad_opt in options_bad: - inst = Estimator(session=session) - with self.assertRaises(ValueError) as exc: - _ = inst.run(self.circuit, observables=self.observables, **bad_opt) - self.assertIn(list(bad_opt.keys())[0], str(exc.exception)) - - @ddt class TestEstimatorV2(IBMTestCase): """Class for testing the Estimator class.""" diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py deleted file mode 100644 index 859793ad9..000000000 --- a/test/unit/test_ibm_primitives.py +++ /dev/null @@ -1,974 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for primitive classes.""" - -import copy -import os -from unittest.mock import MagicMock, patch -from dataclasses import asdict -from typing import Dict - -from ddt import data, ddt -from qiskit import transpile, pulse -from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.pulse.library import Gaussian -from qiskit.quantum_info import SparsePauliOp -from qiskit_aer.noise import NoiseModel - -from qiskit_ibm_runtime.fake_provider import FakeManila, FakeSherbrooke -from qiskit_ibm_runtime import ( - Sampler, - Estimator, - Options, - Session, -) -from qiskit_ibm_runtime.ibm_backend import IBMBackend -from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION -from qiskit_ibm_runtime.exceptions import IBMInputValueError - -from ..ibm_test_case import IBMTestCase -from ..utils import ( - dict_paritally_equal, - flat_dict_partially_equal, - dict_keys_equal, - create_faulty_backend, - bell, - get_mocked_backend, - get_primitive_inputs, -) - - -class MockSession(Session): - """Mock for session class""" - - _circuits_map: Dict[str, QuantumCircuit] = {} - _instance = None - - -@ddt -class TestPrimitives(IBMTestCase): - """Class for testing the Sampler and Estimator classes.""" - - @classmethod - def setUpClass(cls): - cls.qx = bell() - cls.obs = SparsePauliOp.from_list([("IZ", 1)]) - return super().setUpClass() - - def tearDown(self) -> None: - super().tearDown() - _DEFAULT_SESSION.set(None) - - def test_dict_options(self): - """Test passing a dictionary as options.""" - options_vars = [ - {}, - { - "resilience_level": 1, - "transpilation": {"initial_layout": [1, 2]}, - "execution": {"shots": 100, "init_qubits": True}, - }, - {"optimization_level": 2}, - {"transpilation": {}}, - ] - primitives = [Sampler, Estimator] - backend = get_mocked_backend() - for cls in primitives: - for options in options_vars: - with self.subTest(primitive=cls, options=options): - inst = cls(backend=backend, options=options) - expected = asdict(Options()) - self._update_dict(expected, copy.deepcopy(options)) - self.assertDictEqual(expected, inst.options.__dict__) - - def test_runtime_options(self): - """Test RuntimeOptions specified as primitive options.""" - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - env_vars = [ - {"log_level": "DEBUG"}, - {"job_tags": ["foo", "bar"]}, - ] - for cls in primitives: - for env in env_vars: - with self.subTest(primitive=cls, env=env): - options = Options(environment=env) - inst = cls(backend=backend, options=options) - inst.run(**get_primitive_inputs(inst, backend=backend)) - run_options = backend.service.run.call_args.kwargs["options"] - for key, val in env.items(): - self.assertEqual(run_options[key], val) - - def test_options_copied(self): - """Test modifying original options does not affect primitives.""" - backend = get_mocked_backend() - options = Options() - primitives = [Sampler, Estimator] - for cls in primitives: - with self.subTest(primitive=cls): - options.transpilation.skip_transpilation = True - inst = cls(backend=backend, options=options) - options.transpilation.skip_transpilation = False - self.assertTrue(inst.options.get("transpilation").get("skip_transpilation")) - - @data(Sampler, Estimator) - def test_init_with_backend_str(self, primitive): - """Test initializing a primitive with a backend name.""" - backend_name = "ibm_gotham" - mock_backend = get_mocked_backend(name=backend_name) - mock_service_inst = mock_backend.service - - class MockQRTService: - """Mock class used to create a new QiskitRuntimeService.""" - - global_service = None - - def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument - return mock_service_inst - - with patch("qiskit_ibm_runtime.base_primitive.QiskitRuntimeService", new=MockQRTService): - inst = primitive(backend=backend_name) - self.assertIsNone(inst.session) - inst.run(self.qx, observables=self.obs) - mock_service_inst._run.assert_called_once() - runtime_options = mock_service_inst._run.call_args.kwargs["options"] - self.assertEqual(runtime_options["backend"], mock_backend) - - def test_init_with_session_backend_str(self): - """Test initializing a primitive with a backend name using session.""" - primitives = [Sampler, Estimator] - backend_name = "ibm_gotham" - - for cls in primitives: - with self.subTest(primitive=cls), patch( - "qiskit_ibm_runtime.base_primitive.QiskitRuntimeService" - ): - with self.assertRaises(ValueError) as exc: - inst = cls(session=backend_name) - self.assertIsNone(inst.session) - self.assertIn("session must be of type Session or None", str(exc.exception)) - - def test_init_with_backend_instance(self): - """Test initializing a primitive with a backend instance.""" - primitives = [Sampler, Estimator] - backend = get_mocked_backend() - service = backend.service - - for cls in primitives: - with self.subTest(primitive=cls): - service.reset_mock() - inst = cls(backend=backend) - self.assertIsNone(inst.session) - inst.run(**get_primitive_inputs(inst)) - service.run.assert_called_once() - runtime_options = service.run.call_args.kwargs["options"] - self.assertEqual(runtime_options["backend"], backend) - - with self.assertRaises(ValueError) as exc: - inst = cls(session=backend) - self.assertIsNone(inst.session) - self.assertIn("session must be of type Session or None", str(exc.exception)) - - def test_init_with_backend_session(self): - """Test initializing a primitive with both backend and session.""" - primitives = [Sampler, Estimator] - session = MagicMock(spec=MockSession) - backend_name = "ibm_gotham" - backend = get_mocked_backend(backend_name) - session._backend = backend - - for cls in primitives: - with self.subTest(primitive=cls): - session.reset_mock() - inst = cls(session=session, backend=backend_name) - self.assertIsNotNone(inst.session) - inst.run(**get_primitive_inputs(inst, backend=backend)) - session.run.assert_called_once() - - def test_default_session_context_manager(self): - """Test getting default session within context manager.""" - # service = MagicMock() - backend_name = "ibm_gotham" - backend = get_mocked_backend(backend_name) - primitives = [Sampler, Estimator] - - for cls in primitives: - with self.subTest(primitive=cls): - with Session(service=backend.service, backend=backend_name) as session: - inst = cls() - self.assertEqual(inst.session, session) - self.assertEqual(inst.session.backend(), backend_name) - - def test_default_session_cm_new_backend(self): - """Test using a different backend within context manager.""" - cm_backend_name = "ibm_metropolis" - primitives = [Sampler, Estimator] - for cls in primitives: - with self.subTest(primitive=cls): - backend_name = "ibm_gotham" - backend = get_mocked_backend(name=backend_name) - with Session(service=backend.service, backend=cm_backend_name): - inst = cls(backend=backend) - self.assertIsNone(inst.session) - inst.run(**get_primitive_inputs(inst, backend=backend)) - backend.service.run.assert_called_once() - runtime_options = backend.service.run.call_args.kwargs["options"] - self.assertEqual(runtime_options["backend"], backend) - - def test_no_session(self): - """Test running without session.""" - primitives = [Sampler, Estimator] - model_backend = FakeManila() - for cls in primitives: - with self.subTest(primitive=cls): - service = MagicMock() - backend = IBMBackend( - configuration=model_backend.configuration(), - service=service, - api_client=MagicMock(), - ) - inst = cls(backend) - inst.run(self.qx, observables=self.obs) - self.assertIsNone(inst.session) - service._run.assert_called_once() - kwargs_list = service._run.call_args.kwargs - self.assertNotIn("session_id", kwargs_list) - self.assertNotIn("start_session", kwargs_list) - - def test_run_default_options(self): - """Test run using default options.""" - backend = get_mocked_backend() - options_vars = [ - (Options(resilience_level=1), {"resilience_settings": {"level": 1}}), - ( - Options(optimization_level=3), - {"transpilation_settings": {"optimization_settings": {"level": 3}}}, - ), - ( - { - "transpilation": {"initial_layout": [1, 2]}, - "execution": {"shots": 100}, - }, - { - "transpilation_settings": {"initial_layout": [1, 2]}, - "run_options": {"shots": 100}, - }, - ), - ] - primitives = [Sampler, Estimator] - for cls in primitives: - for options, expected in options_vars: - with self.subTest(primitive=cls, options=options): - inst = cls(backend=backend, options=options) - inst.run(**get_primitive_inputs(inst, backend=backend)) - inputs = backend.service.run.call_args.kwargs["inputs"] - self._assert_dict_partially_equal(inputs, expected) - - def test_run_updated_default_options(self): - """Test run using updated default options.""" - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - for cls in primitives: - with self.subTest(primitive=cls): - inst = cls(backend=backend) - inst.set_options(resilience_level=1, optimization_level=2, shots=99) - inst.run(**get_primitive_inputs(inst, backend)) - inputs = backend.service.run.call_args.kwargs["inputs"] - self._assert_dict_partially_equal( - inputs, - { - "resilience_settings": {"level": 1}, - "transpilation_settings": {"optimization_settings": {"level": 2}}, - "run_options": {"shots": 99}, - }, - ) - - def test_run_overwrite_options(self): - """Test run using overwritten options.""" - backend = get_mocked_backend() - options_vars = [ - ({"resilience_level": 1}, {"resilience_settings": {"level": 1}}), - ({"shots": 200}, {"run_options": {"shots": 200}}), - ( - {"optimization_level": 3}, - {"transpilation_settings": {"optimization_settings": {"level": 3}}}, - ), - ( - {"initial_layout": [1, 2], "optimization_level": 2}, - { - "transpilation_settings": { - "optimization_settings": {"level": 2}, - "initial_layout": [1, 2], - } - }, - ), - ] - primitives = [Sampler, Estimator] - for cls in primitives: - for options, expected in options_vars: - with self.subTest(primitive=cls, options=options): - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend), **options) - inputs = backend.service.run.call_args.kwargs["inputs"] - self._assert_dict_partially_equal(inputs, expected) - self.assertDictEqual(inst.options.__dict__, asdict(Options())) - - def test_run_overwrite_runtime_options(self): - """Test run using overwritten runtime options.""" - backend = get_mocked_backend() - options_vars = [ - {"log_level": "DEBUG"}, - {"job_tags": ["foo", "bar"]}, - {"max_execution_time": 600}, - {"log_level": "INFO", "max_execution_time": 800}, - ] - primitives = [Sampler, Estimator] - for cls in primitives: - for options in options_vars: - with self.subTest(primitive=cls, options=options): - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend), **options) - rt_options = backend.service.run.call_args.kwargs["options"] - self._assert_dict_partially_equal(rt_options, options) - - def test_run_kwarg_options(self): - """Test specifying arbitrary options in run.""" - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - for cls in primitives: - with self.subTest(primitive=cls): - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend), foo="foo") - inputs = backend.service.run.call_args.kwargs["inputs"] - self.assertEqual(inputs.get("foo"), "foo") - - def test_run_multiple_different_options(self): - """Test multiple runs with different options.""" - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - for cls in primitives: - with self.subTest(primitive=cls): - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend), shots=100) - inst.run(**get_primitive_inputs(inst, backend), shots=200) - kwargs_list = backend.service.run.call_args_list - for idx, shots in zip([0, 1], [100, 200]): - self.assertEqual(kwargs_list[idx][1]["inputs"]["run_options"]["shots"], shots) - self.assertDictEqual(inst.options.__dict__, asdict(Options())) - - def test_run_same_session(self): - """Test multiple runs within a session.""" - num_runs = 5 - primitives = [Sampler, Estimator] - backend = get_mocked_backend() - for idx in range(num_runs): - cls = primitives[idx % 2] - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend)) - self.assertEqual(backend.service.run.call_count, num_runs) - - def test_set_options(self): - """Test set options.""" - options = Options(optimization_level=1, execution={"shots": 100}) - new_options = [ - ({"optimization_level": 2}, Options()), - ({"optimization_level": 3, "shots": 200}, Options()), - ] - - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - for cls in primitives: - for new_opt, new_str in new_options: - with self.subTest(primitive=cls, new_opt=new_opt): - inst = cls(backend=backend, options=options) - inst.set_options(**new_opt) - # Make sure the values are equal. - inst_options = inst.options.__dict__ - self.assertTrue( - flat_dict_partially_equal(inst_options, new_opt), - f"inst_options={inst_options}, new_opt={new_opt}", - ) - # Make sure the structure didn't change. - self.assertTrue( - dict_keys_equal(inst_options, asdict(new_str)), - f"inst_options={inst_options}, new_str={new_str}", - ) - - def test_accept_level_1_options(self): - """Test initializing options properly when given on level 1.""" - - options_dicts = [ - {}, - {"shots": 10}, - {"seed_simulator": 123}, - {"skip_transpilation": True, "log_level": "ERROR"}, - {"initial_layout": [1, 2], "shots": 100, "noise_factors": (0, 2, 4)}, - ] - - expected_list = [Options(), Options(), Options(), Options(), Options()] - expected_list[1].execution.shots = 10 - expected_list[2].simulator.seed_simulator = 123 - expected_list[3].transpilation.skip_transpilation = True - expected_list[3].environment.log_level = "ERROR" - expected_list[4].transpilation.initial_layout = [1, 2] - expected_list[4].execution.shots = 100 - expected_list[4].resilience.noise_factors = (0, 2, 4) - - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - for cls in primitives: - for opts, expected in zip(options_dicts, expected_list): - with self.subTest(primitive=cls, options=opts): - inst1 = cls(backend=backend, options=opts) - inst2 = cls(backend=backend, options=expected) - # Make sure the values are equal. - inst1_options = inst1.options.__dict__ - expected_dict = inst2.options.__dict__ - self.assertTrue( - dict_paritally_equal(inst1_options, expected_dict), - f"inst_options={inst1_options}, options={opts}", - ) - # Make sure the structure didn't change. - self.assertTrue( - dict_keys_equal(inst1_options, expected_dict), - f"inst_options={inst1_options}, expected={expected_dict}", - ) - - def test_default_error_levels(self): - """Test the correct default error levels are used.""" - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - noise_model = NoiseModel.from_backend(FakeManila()) - for cls in primitives: - with self.subTest(primitive=cls): - options = Options( - simulator={"noise_model": noise_model}, - ) - inst = cls(backend=backend, options=options) - inst.run(**get_primitive_inputs(inst, backend)) - inputs = backend.service.run.call_args.kwargs["inputs"] - self.assertEqual( - inputs["transpilation_settings"]["optimization_settings"]["level"], - Options._DEFAULT_OPTIMIZATION_LEVEL, - ) - self.assertEqual( - inputs["resilience_settings"]["level"], - Options._DEFAULT_RESILIENCE_LEVEL, - ) - - inst = cls(backend=backend) - inst.run(**get_primitive_inputs(inst, backend)) - inputs = backend.service.run.call_args.kwargs["inputs"] - self.assertEqual( - inputs["transpilation_settings"]["optimization_settings"]["level"], - Options._DEFAULT_OPTIMIZATION_LEVEL, - ) - self.assertEqual( - inputs["resilience_settings"]["level"], - Options._DEFAULT_RESILIENCE_LEVEL, - ) - - config = FakeManila().configuration().to_dict() - config["simulator"] = True - sim = get_mocked_backend(configuration=config) - inst = cls(backend=sim) - inst.run(**get_primitive_inputs(inst, sim)) - inputs = sim.service.run.call_args.kwargs["inputs"] - self.assertEqual( - inputs["transpilation_settings"]["optimization_settings"]["level"], - 1, - ) - self.assertEqual(inputs["resilience_settings"]["level"], 0) - - def test_resilience_options(self): - """Test resilience options.""" - options_dicts = [ - {"resilience": {"extrapolator": "NoExtrapolator"}}, - { - "resilience": { - "extrapolator": "QuarticExtrapolator", - "noise_factors": [1, 2, 3, 4], - }, - }, - { - "resilience": { - "extrapolator": "CubicExtrapolator", - "noise_factors": [1, 2, 3], - }, - }, - ] - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - - for cls in primitives: - for opts_dict in options_dicts: - # When this environment variable is set, validation is turned off - try: - os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] = "1" - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - finally: - # Delete environment variable to validate input - del os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] - with self.assertRaises(ValueError) as exc: - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - self.assertIn(list(opts_dict["resilience"].values())[0], str(exc.exception)) - if len(opts_dict["resilience"].keys()) > 1: - self.assertIn(list(opts_dict["resilience"].keys())[1], str(exc.exception)) - - def test_environment_options(self): - """Test environment options.""" - options_dicts = [ - {"environment": {"log_level": "NoLogLevel"}}, - ] - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - - for cls in primitives: - for opts_dict in options_dicts: - # When this environment variable is set, validation is turned off - try: - os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] = "1" - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - finally: - # Delete environment variable to validate input - del os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] - with self.assertRaises(ValueError) as exc: - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - self.assertIn(list(opts_dict["environment"].values())[0], str(exc.exception)) - - def test_transpilation_options(self): - """Test transpilation options.""" - options_dicts = [ - {"transpilation": {"layout_method": "NoLayoutMethod"}}, - {"transpilation": {"routing_method": "NoRoutingMethod"}}, - {"transpilation": {"approximation_degree": 1.1}}, - ] - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - - for cls in primitives: - for opts_dict in options_dicts: - # When this environment variable is set, validation is turned off - try: - os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] = "1" - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - finally: - # Delete environment variable to validate input - del os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] - with self.assertRaises(ValueError) as exc: - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - self.assertIn(list(opts_dict["transpilation"].keys())[0], str(exc.exception)) - - def test_max_execution_time_options(self): - """Test max execution time options.""" - options_dicts = [ - {"max_execution_time": Options._MAX_EXECUTION_TIME + 1}, - ] - backend = get_mocked_backend() - primitives = [Sampler, Estimator] - - for cls in primitives: - for opts_dict in options_dicts: - # When this environment variable is set, validation is turned off - try: - os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] = "1" - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - finally: - # Delete environment variable to validate input - del os.environ["QISKIT_RUNTIME_SKIP_OPTIONS_VALIDATION"] - with self.assertRaises(ValueError) as exc: - inst = cls(backend=backend, options=opts_dict) - inst.run(**get_primitive_inputs(inst, backend)) - self.assertIn( - "max_execution_time must be below 28800 seconds", - str(exc.exception), - ) - - def test_raise_faulty_qubits(self): - """Test faulty qubits is raised.""" - fake_backend = FakeManila() - num_qubits = fake_backend.configuration().num_qubits - circ = QuantumCircuit(num_qubits, num_qubits) - for i in range(num_qubits): - circ.x(i) - transpiled = transpile(circ, backend=fake_backend) - observable = SparsePauliOp("Z" * num_qubits) - - faulty_qubit = 4 - ibm_backend = create_faulty_backend(fake_backend, faulty_qubit=faulty_qubit) - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with self.assertRaises(ValueError) as err: - estimator.run(transpiled, observable, skip_transpilation=True) - self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception)) - - transpiled.measure_all() - with self.assertRaises(ValueError) as err: - sampler.run(transpiled, skip_transpilation=True) - self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception)) - - def test_raise_faulty_qubits_many(self): - """Test faulty qubits is raised if one circuit uses it.""" - fake_backend = FakeManila() - num_qubits = fake_backend.configuration().num_qubits - - circ1 = QuantumCircuit(1, 1) - circ1.x(0) - circ2 = QuantumCircuit(num_qubits, num_qubits) - for i in range(num_qubits): - circ2.x(i) - transpiled = transpile([circ1, circ2], backend=fake_backend) - observable = SparsePauliOp("Z" * num_qubits) - - faulty_qubit = 4 - ibm_backend = create_faulty_backend(fake_backend, faulty_qubit=faulty_qubit) - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with self.assertRaises(ValueError) as err: - estimator.run(transpiled, [observable, observable], skip_transpilation=True) - self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception)) - - for circ in transpiled: - circ.measure_all() - - with self.assertRaises(ValueError) as err: - sampler.run(transpiled, skip_transpilation=True) - self.assertIn(f"faulty qubit {faulty_qubit}", str(err.exception)) - - def test_raise_faulty_edge(self): - """Test faulty edge is raised.""" - fake_backend = FakeManila() - num_qubits = fake_backend.configuration().num_qubits - circ = QuantumCircuit(num_qubits, num_qubits) - for i in range(num_qubits - 2): - circ.cx(i, i + 1) - transpiled = transpile(circ, backend=fake_backend) - observable = SparsePauliOp("Z" * num_qubits) - - edge_qubits = [0, 1] - ibm_backend = create_faulty_backend(fake_backend, faulty_edge=("cx", edge_qubits)) - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with self.assertRaises(ValueError) as err: - estimator.run(transpiled, observable, skip_transpilation=True) - self.assertIn("cx", str(err.exception)) - self.assertIn(f"faulty edge {tuple(edge_qubits)}", str(err.exception)) - - transpiled.measure_all() - with self.assertRaises(ValueError) as err: - sampler.run(transpiled, skip_transpilation=True) - self.assertIn("cx", str(err.exception)) - self.assertIn(f"faulty edge {tuple(edge_qubits)}", str(err.exception)) - - def test_faulty_qubit_not_used(self): - """Test faulty qubit is not raise if not used.""" - fake_backend = FakeManila() - circ = QuantumCircuit(2, 2) - for i in range(2): - circ.x(i) - transpiled = transpile(circ, backend=fake_backend, initial_layout=[0, 1]) - observable = SparsePauliOp("Z" * fake_backend.configuration().num_qubits) - - faulty_qubit = 4 - ibm_backend = create_faulty_backend(fake_backend, faulty_qubit=faulty_qubit) - - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with patch.object(Session, "run") as mock_run: - estimator.run(transpiled, observable, skip_transpilation=True) - mock_run.assert_called_once() - - transpiled.measure_active() - with patch.object(Session, "run") as mock_run: - sampler.run(transpiled, skip_transpilation=True) - mock_run.assert_called_once() - - def test_faulty_edge_not_used(self): - """Test faulty edge is not raised if not used.""" - fake_backend = FakeManila() - coupling_map = fake_backend.configuration().coupling_map - - circ = QuantumCircuit(2, 2) - circ.cx(0, 1) - - transpiled = transpile(circ, backend=fake_backend, initial_layout=coupling_map[0]) - observable = SparsePauliOp("Z" * fake_backend.configuration().num_qubits) - - edge_qubits = coupling_map[-1] - ibm_backend = create_faulty_backend(fake_backend, faulty_edge=("cx", edge_qubits)) - - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with patch.object(Session, "run") as mock_run: - estimator.run(transpiled, observable, skip_transpilation=True) - mock_run.assert_called_once() - - transpiled.measure_all() - with patch.object(Session, "run") as mock_run: - sampler.run(transpiled, skip_transpilation=True) - mock_run.assert_called_once() - - def test_no_raise_skip_transpilation(self): - """Test faulty qubits and edges are not raise if not skipping.""" - fake_backend = FakeManila() - num_qubits = fake_backend.configuration().num_qubits - circ = QuantumCircuit(num_qubits, num_qubits) - for i in range(num_qubits - 2): - circ.cx(i, i + 1) - transpiled = transpile(circ, backend=fake_backend) - observable = SparsePauliOp("Z" * num_qubits) - - edge_qubits = [0, 1] - ibm_backend = create_faulty_backend( - fake_backend, faulty_qubit=0, faulty_edge=("cx", edge_qubits) - ) - - service = MagicMock() - service.backend.return_value = ibm_backend - session = Session(service=service, backend=fake_backend.name()) - sampler = Sampler(session=session) - estimator = Estimator(session=session) - - with patch.object(Session, "run") as mock_run: - estimator.run(transpiled, observable) - mock_run.assert_called_once() - - transpiled.measure_all() - with patch.object(Session, "run") as mock_run: - sampler.run(transpiled) - mock_run.assert_called_once() - - @data(Sampler, Estimator) - def test_abstract_circuits(self, primitive): - """Test passing in abstract circuit.""" - backend = get_mocked_backend() - inst = primitive(backend=backend) - - circ = QuantumCircuit(3, 3) - circ.cx(0, 2) - run_input = {"circuits": circ} - if isinstance(inst, Estimator): - run_input["observables"] = SparsePauliOp("ZZZ") - else: - circ.measure_all() - - with self.assertRaisesRegex(IBMInputValueError, "target hardware"): - inst.run(**run_input) - - @data(Sampler, Estimator) - def test_abstract_circuits_backend_no_coupling_map(self, primitive): - """Test passing in abstract circuits to a backend with no coupling map.""" - - config = FakeManila().configuration().to_dict() - for gate in config["gates"]: - gate.pop("coupling_map", None) - backend = get_mocked_backend(configuration=config) - - inst = primitive(backend=backend) - circ = QuantumCircuit(2, 2) - circ.cx(0, 1) - transpiled = transpile(circ, backend=backend) - run_input = {"circuits": transpiled} - if isinstance(inst, Estimator): - run_input["observables"] = SparsePauliOp("ZZ") - else: - transpiled.measure_all() - - inst.run(**run_input) - - @data(Sampler, Estimator) - def test_pulse_gates_is_isa(self, primitive): - """Test passing circuits with pulse gates is considered ISA.""" - backend = get_mocked_backend() - inst = primitive(backend=backend) - - circuit = QuantumCircuit(1) - circuit.h(0) - with pulse.build(backend, name="hadamard") as h_q0: - pulse.play(Gaussian(duration=64, amp=0.5, sigma=8), pulse.drive_channel(0)) - circuit.add_calibration("h", [0], h_q0) - - run_input = {"circuits": circuit} - if isinstance(inst, Estimator): - run_input["observables"] = SparsePauliOp("Z") - else: - circuit.measure_all() - - inst.run(**run_input) - - @data(Sampler, Estimator) - def test_dynamic_circuit_is_isa(self, primitive): - """Test passing dynmaic circuits is considered ISA.""" - # pylint: disable=not-context-manager - # pylint: disable=invalid-name - sherbrooke = FakeSherbrooke() - config = sherbrooke._get_conf_dict_from_json() - config["supported_instructions"] += ["for_loop", "switch_case", "while_loop"] - - backend = get_mocked_backend( - configuration=config, - properties=sherbrooke._set_props_dict_from_json(), - defaults=sherbrooke._set_defs_dict_from_json(), - ) - - inst = primitive(backend=backend) - - qubits = QuantumRegister(3) - clbits = ClassicalRegister(3) - circuit = QuantumCircuit(qubits, clbits) - (q0, q1, q2) = qubits - (c0, c1, c2) = clbits - - circuit.x(q0) - circuit.measure(q0, c0) - with circuit.if_test((c0, 1)): - circuit.x(q0) - - circuit.measure(q1, c1) - with circuit.switch(c1) as case: - with case(0): - circuit.x(q0) - with case(1): - circuit.x(q1) - - circuit.measure(q1, c1) - circuit.measure(q2, c2) - with circuit.while_loop((clbits, 0b111)): - circuit.rz(1.5, q1) - circuit.rz(1.5, q2) - circuit.measure(q1, c1) - circuit.measure(q2, c2) - - with circuit.for_loop(range(2)) as _: - circuit.x(q0) - - circuit = transpile(circuit, backend=backend) - run_input = {"circuits": circuit} - if isinstance(inst, Estimator): - run_input["observables"] = SparsePauliOp("ZZZ").apply_layout(circuit.layout) - - inst.run(**run_input) - - def _update_dict(self, dict1, dict2): - for key, val in dict1.items(): - if isinstance(val, dict): - self._update_dict(val, dict2.pop(key, {})) - elif key in dict2.keys(): - dict1[key] = dict2.pop(key) - - def _assert_dict_partially_equal(self, dict1, dict2): - """Assert all keys in dict2 are in dict1 and have same values.""" - self.assertTrue( - dict_paritally_equal(dict1, dict2), - f"{dict1} and {dict2} not partially equal.", - ) - - def test_qctrl_supported_values_for_options(self): - """Test exception when options levels not supported.""" - no_resilience_options = { - "noise_factors": None, - "extrapolator": None, - } - - options_good = [ - # Minium working settings - {}, - # No warnings, we need resilience options here because by default they are getting populated. - {"resilience": no_resilience_options}, - # Arbitrary approximation degree (issues warning) - {"approximation_degree": 1}, - # Arbitrary resilience options(issue warning) - { - "resilience_level": 1, - "resilience": {"noise_factors": (1, 1, 3)}, - "approximation_degree": 1, - }, - # Resilience level > 1 (issue warning) - {"resilience_level": 2}, - # Optimization level = 1,2 (issue warning) - {"optimization_level": 1}, - {"optimization_level": 2}, - # Skip transpilation level(issue warning) - {"skip_transpilation": True}, - ] - backend = get_mocked_backend() - backend._service._channel_strategy = "q-ctrl" - primitives = [Sampler, Estimator] - for cls in primitives: - for options in options_good: - with self.subTest(msg=f"{cls}, {options}"): - inst = cls(backend=backend) - _ = inst.run(**get_primitive_inputs(inst, backend), **options) - - def test_qctrl_unsupported_values_for_options(self): - """Test exception when options levels are not supported.""" - options_bad = [ - # Bad resilience levels - ({"resilience_level": 0}, "resilience level"), - # Bad optimization level - ({"optimization_level": 0}, "optimization level"), - ] - backend = get_mocked_backend() - backend._service._channel_strategy = "q-ctrl" - primitives = [Sampler, Estimator] - for cls in primitives: - for bad_opt, expected_message in options_bad: - with self.subTest(msg=bad_opt): - inst = cls(backend=backend) - with self.assertRaises(ValueError) as exc: - _ = inst.run(**get_primitive_inputs(inst, backend), **bad_opt) - self.assertIn(expected_message, str(exc.exception)) - - @data(Sampler, Estimator) - def test_qctrl_abstract_circuit(self, primitive): - """Test q-ctrl can still accept abstract circuits.""" - backend = get_mocked_backend() - backend._service._channel_strategy = "q-ctrl" - inst = primitive(backend=backend) - - circ = QuantumCircuit(3, 3) - circ.cx(0, 2) - run_input = {"circuits": circ} - if isinstance(inst, Estimator): - run_input["observables"] = SparsePauliOp("ZZZ") - else: - circ.measure_all() - - inst.run(**run_input) diff --git a/test/unit/test_local_mode.py b/test/unit/test_local_mode.py index 2eaec29f3..8a37022c2 100644 --- a/test/unit/test_local_mode.py +++ b/test/unit/test_local_mode.py @@ -18,8 +18,6 @@ from qiskit_aer import AerSimulator from qiskit.primitives import ( - EstimatorResult, - SamplerResult, PrimitiveResult, PubResult, SamplerPubResult, @@ -28,9 +26,6 @@ from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 from qiskit_ibm_runtime import ( - Sampler, - Estimator, - Options, Session, Batch, SamplerV2, @@ -44,93 +39,6 @@ ) -@ddt -class TestLocalModeV1(IBMTestCase): - """Class for testing local mode for v1 primitives.""" - - @combine(backend=[FakeManila(), FakeManilaV2(), AerSimulator()], num_sets=[1, 3]) - def test_v1_sampler(self, backend, num_sets): - """Test V1 Sampler on a local backend.""" - inst = Sampler(backend=backend) - job = inst.run(**get_primitive_inputs(inst, backend=backend, num_sets=num_sets)) - result = job.result() - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), num_sets) - self.assertEqual(len(result.metadata), num_sets) - - @combine(backend=[FakeManila(), FakeManilaV2(), AerSimulator()], num_sets=[1, 3]) - def test_v1_estimator(self, backend, num_sets): - """Test V1 Estimator on a local backend.""" - inst = Estimator(backend=backend) - job = inst.run(**get_primitive_inputs(inst, backend=backend, num_sets=num_sets)) - result = job.result() - self.assertIsInstance(result, EstimatorResult) - self.assertEqual(len(result.values), num_sets) - self.assertEqual(len(result.metadata), num_sets) - - @data(FakeManila(), FakeManilaV2(), AerSimulator()) - def test_v1_sampler_with_accepted_options(self, backend): - """Test V1 sampler with accepted options.""" - shots = 2000 - options = Options( - execution={"shots": shots}, - transpilation={"skip_transpilation": True}, - simulator={"seed_simulator": 42}, - ) - inst = Sampler(backend=backend, options=options) - job = inst.run(**get_primitive_inputs(inst, backend=backend)) - result = job.result() - self.assertEqual(result.metadata[0]["shots"], shots) - self.assertDictEqual(result.quasi_dists[0], {1: 0.002, 2: 0.001, 0: 0.504, 3: 0.493}) - - @data(FakeManila(), FakeManilaV2(), AerSimulator()) - def test_v1_estimator_with_accepted_options(self, backend): - """Test V1 estimator with accepted options.""" - shots = 2000 - options = Options( - execution={"shots": shots}, - transpilation={"skip_transpilation": True}, - simulator={"seed_simulator": 42}, - ) - inst = Estimator(backend=backend, options=options) - job = inst.run(**get_primitive_inputs(inst, backend=backend)) - result = job.result() - self.assertEqual(result.metadata[0]["shots"], shots) - self.assertEqual(result.values[0], 0.01) - - @combine(primitive=[Sampler, Estimator], backend=[FakeManila(), FakeManilaV2(), AerSimulator()]) - def test_primitve_v1_with_not_accepted_options(self, primitive, backend): - """Test V1 primitive with accepted options.""" - shots = 2000 - options = Options(execution={"shots": shots}, resilience_level=1, max_execution_time=200) - inst = primitive(backend=backend, options=options) - job = inst.run(**get_primitive_inputs(inst, backend=backend)) - result = job.result() - self.assertEqual(result.metadata[0]["shots"], shots) - - @combine(session_cls=[Session, Batch], backend=[FakeManila(), FakeManilaV2(), AerSimulator()]) - def test_sampler_v1_session(self, session_cls, backend): - """Testing running v1 sampler inside session.""" - with session_cls(backend=backend) as session: - inst = Sampler(session=session) - job = inst.run(**get_primitive_inputs(inst, backend=backend)) - result = job.result() - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 1) - self.assertEqual(len(result.metadata), 1) - - @combine(session_cls=[Session, Batch], backend=[FakeManila(), FakeManilaV2(), AerSimulator()]) - def test_estimator_v1_session(self, session_cls, backend): - """Testing running v1 estimator inside session.""" - with session_cls(backend=backend) as session: - inst = Estimator(session=session) - job = inst.run(**get_primitive_inputs(inst, backend=backend)) - result = job.result() - self.assertIsInstance(result, EstimatorResult) - self.assertEqual(len(result.values), 1) - self.assertEqual(len(result.metadata), 1) - - @ddt class TestLocalModeV2(IBMTestCase): """Class for testing local mode for V2 primitives.""" diff --git a/test/unit/test_options.py b/test/unit/test_options.py index 66abc5ca3..1533acdb1 100644 --- a/test/unit/test_options.py +++ b/test/unit/test_options.py @@ -21,224 +21,12 @@ from qiskit.transpiler import CouplingMap from qiskit_aer.noise import NoiseModel -from qiskit_ibm_runtime import Options, RuntimeOptions +from qiskit_ibm_runtime import RuntimeOptions from qiskit_ibm_runtime.options import EstimatorOptions, SamplerOptions -from qiskit_ibm_runtime.utils.qctrl import _warn_and_clean_options from qiskit_ibm_runtime.fake_provider import FakeManila, FakeNairobiV2 from ..ibm_test_case import IBMTestCase -from ..utils import dict_keys_equal, dict_paritally_equal, combine - - -@ddt -class TestOptions(IBMTestCase): - """Class for testing the Options class.""" - - def test_runtime_options(self): - """Test converting runtime options.""" - full_options = RuntimeOptions( - backend="ibm_gotham", - image="foo:bar", - log_level="DEBUG", - instance="h/g/p", - job_tags=["foo", "bar"], - max_execution_time=600, - ) - partial_options = RuntimeOptions(backend="foo", log_level="DEBUG") - - for rt_options in [full_options, partial_options]: - with self.subTest(rt_options=rt_options): - self.assertGreaterEqual( - vars(rt_options).items(), - Options._get_runtime_options(vars(rt_options)).items(), - ) - - def test_program_inputs(self): - """Test converting to program inputs.""" - noise_model = NoiseModel.from_backend(FakeNairobiV2()) - options = Options( # pylint: disable=unexpected-keyword-arg - optimization_level=1, - resilience_level=2, - transpilation={"initial_layout": [1, 2], "skip_transpilation": True}, - execution={"shots": 100}, - environment={"log_level": "DEBUG"}, - simulator={"noise_model": noise_model}, - resilience={"noise_factors": (1, 2, 4)}, - ) - inputs = Options._get_program_inputs(asdict(options)) - - expected = { - "run_options": {"shots": 100, "noise_model": noise_model}, - "transpilation_settings": { - "optimization_settings": {"level": 1}, - "skip_transpilation": True, - "initial_layout": [1, 2], - }, - "resilience_settings": { - "level": 2, - "noise_factors": (1, 2, 4), - }, - } - self.assertTrue( - dict_paritally_equal(inputs, expected), - f"inputs={inputs}, expected={expected}", - ) - - def test_init_options_with_dictionary(self): - """Test initializing options with dictionaries.""" - - options_dicts = [ - {}, - {"resilience_level": 9}, - {"simulator": {"seed_simulator": 42}}, - {"resilience_level": 8, "environment": {"log_level": "WARNING"}}, - { - "transpilation": {"initial_layout": [1, 2], "layout_method": "trivial"}, - "execution": {"shots": 100}, - }, - {"resilience": {"noise_factors": (0, 2, 4)}}, - {"environment": {"log_level": "ERROR"}}, - ] - - for opts_dict in options_dicts: - with self.subTest(opts_dict=opts_dict): - options = asdict(Options(**opts_dict)) - self.assertTrue( - dict_paritally_equal(options, opts_dict), - f"options={options}, opts_dict={opts_dict}", - ) - - # Make sure the structure didn't change. - self.assertTrue(dict_keys_equal(asdict(Options()), options), f"options={options}") - - def test_kwargs_options(self): - """Test specifying arbitrary options.""" - with self.assertRaises(TypeError) as exc: - _ = Options(foo="foo") # pylint: disable=unexpected-keyword-arg - self.assertIn( - "__init__() got an unexpected keyword argument 'foo'", - str(exc.exception), - ) - - def test_unsupported_options(self): - """Test error on unsupported second level options""" - # defining minimal dict of options - options = { - "optimization_level": 1, - "resilience_level": 2, - "transpilation": {"initial_layout": [1, 2], "skip_transpilation": True}, - "execution": {"shots": 100}, - "environment": {"log_level": "DEBUG"}, - "resilience": { - "noise_factors": (0, 2, 4), - "extrapolator": "LinearExtrapolator", - }, - } - Options.validate_options(options) - for opt in ["resilience", "simulator", "transpilation", "execution"]: - temp_options = options.copy() - temp_options[opt] = {"aaa": "bbb"} - with self.assertRaises(ValidationError) as exc: - Options.validate_options(temp_options) - self.assertIn("bbb", str(exc.exception)) - - def test_coupling_map_options(self): - """Check that coupling_map is processed correctly for various types""" - coupling_map = {(1, 0), (2, 1), (0, 1), (1, 2)} - coupling_maps = [ - coupling_map, - list(map(list, coupling_map)), - CouplingMap(coupling_map), - ] - for variant in coupling_maps: - with self.subTest(opts_dict=variant): - options = Options() - options.simulator.coupling_map = variant - inputs = Options._get_program_inputs(asdict(options)) - resulting_cmap = inputs["transpilation_settings"]["coupling_map"] - self.assertEqual(coupling_map, set(map(tuple, resulting_cmap))) - - @data(FakeManila(), FakeNairobiV2()) - def test_simulator_set_backend(self, fake_backend): - """Test Options.simulator.set_backend method.""" - - options = Options() - options.simulator.seed_simulator = 42 - options.simulator.set_backend(fake_backend) - - noise_model = NoiseModel.from_backend(fake_backend) - basis_gates = ( - fake_backend.configuration().basis_gates - if isinstance(fake_backend, BackendV1) - else fake_backend.operation_names - ) - coupling_map = ( - fake_backend.configuration().coupling_map - if isinstance(fake_backend, BackendV1) - else fake_backend.coupling_map - ) - - expected_options = Options() - expected_options.simulator = { - "noise_model": noise_model, - "basis_gates": basis_gates, - "coupling_map": coupling_map, - "seed_simulator": 42, - } - self.assertDictEqual(asdict(options), asdict(expected_options)) - - def test_qctrl_overrides(self): - """Test override of options""" - all_test_options = [ - ( - { - "optimization_level": 2, - "transpilation": {"approximation_degree": 1}, - "resilience_level": 3, - "resilience": { - "noise_factors": (1, 3, 5), - "extrapolator": "Linear", - }, - }, - { - "optimization_level": 3, - "transpilation": {"approximation_degree": 0}, - "resilience_level": 1, - "resilience": { - "noise_factors": None, - "extrapolator": None, - }, - }, - ), - ( - { - "optimization_level": 0, - "transpilation": {"approximation_degree": 1, "skip_transpilation": True}, - "resilience_level": 1, - }, - { - "optimization_level": 3, - "transpilation": {"approximation_degree": 0, "skip_transpilation": False}, - "resilience_level": 1, - }, - ), - ( - { - "optimization_level": 0, - "transpilation": {"skip_transpilation": True}, - "resilience_level": 1, - }, - { - "optimization_level": 3, - "transpilation": {"skip_transpilation": False}, - "resilience_level": 1, - }, - ), - ] - for option, expected_ in all_test_options: - with self.subTest(msg=f"{option}"): - _warn_and_clean_options(option) - self.assertEqual(expected_, option) +from ..utils import combine @ddt diff --git a/test/unit/test_options_utils.py b/test/unit/test_options_utils.py index 5a3d1d325..b12fac552 100644 --- a/test/unit/test_options_utils.py +++ b/test/unit/test_options_utils.py @@ -16,9 +16,7 @@ from ddt import data, ddt -from qiskit_ibm_runtime import Options from qiskit_ibm_runtime.options.utils import ( - merge_options, Unset, remove_dict_unset_values, remove_empty_dict, @@ -27,43 +25,13 @@ from qiskit_ibm_runtime.options import EstimatorOptions, SamplerOptions from ..ibm_test_case import IBMTestCase -from ..utils import dict_keys_equal, flat_dict_partially_equal, dict_paritally_equal +from ..utils import dict_keys_equal, dict_paritally_equal @ddt class TestOptionsUtils(IBMTestCase): """Class for testing the options.utils.""" - def test_merge_v1options(self): - """Test merging options.""" - options_vars = [ - {}, - {"resilience_level": 9}, - {"resilience_level": 8, "transpilation": {"initial_layout": [1, 2]}}, - {"shots": 99, "seed_simulator": 42}, - {"resilience_level": 99, "shots": 98, "initial_layout": [3, 4]}, - { - "initial_layout": [1, 2], - "transpilation": {"layout_method": "trivial"}, - "log_level": "INFO", - }, - ] - for new_ops in options_vars: - with self.subTest(new_ops=new_ops): - options = Options() - combined = merge_options(asdict(options), new_ops) - - # Make sure the values are equal. - self.assertTrue( - flat_dict_partially_equal(combined, new_ops), - f"new_ops={new_ops}, combined={combined}", - ) - # Make sure the structure didn't change. - self.assertTrue( - dict_keys_equal(combined, asdict(options)), - f"options={options}, combined={combined}", - ) - def test_merge_estimator_options(self): """Test merging estimator options.""" options_vars = [ diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index 697103958..181142e4b 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -17,40 +17,17 @@ from ddt import data, ddt, named_data import numpy as np -from qiskit import QuantumCircuit, transpile, QuantumRegister, ClassicalRegister +from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.circuit.library import RealAmplitudes -from qiskit_ibm_runtime import Sampler, Session, SamplerV2, SamplerOptions, IBMInputValueError +from qiskit_ibm_runtime import Session, SamplerV2, SamplerOptions, IBMInputValueError from qiskit_ibm_runtime.fake_provider import FakeFractionalBackend, FakeSherbrooke from ..ibm_test_case import IBMTestCase -from ..utils import bell, MockSession, dict_paritally_equal, get_mocked_backend, transpile_pubs +from ..utils import MockSession, dict_paritally_equal, get_mocked_backend, transpile_pubs from .mock.fake_runtime_service import FakeRuntimeService -class TestSampler(IBMTestCase): - """Class for testing the Sampler class.""" - - def test_unsupported_values_for_sampler_options(self): - """Test exception when options levels are not supported.""" - options_bad = [ - {"resilience_level": 2, "optimization_level": 3}, - {"optimization_level": 4, "resilience_level": 1}, - ] - backend = get_mocked_backend() - circuit = transpile(bell(), backend=backend) - - with Session( - service=FakeRuntimeService(channel="ibm_quantum", token="abc"), - backend="common_backend", - ) as session: - for bad_opt in options_bad: - inst = Sampler(session=session) - with self.assertRaises(ValueError) as exc: - _ = inst.run(circuit, **bad_opt) - self.assertIn(list(bad_opt.keys())[0], str(exc.exception)) - - @ddt class TestSamplerV2(IBMTestCase): """Class for testing the Estimator class.""" diff --git a/test/utils.py b/test/utils.py index 896a9e528..654cb56c5 100644 --- a/test/utils.py +++ b/test/utils.py @@ -24,7 +24,6 @@ from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Parameter from qiskit.compiler import transpile -from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.backend import Backend from qiskit.quantum_info import SparsePauliOp, Pauli @@ -33,8 +32,6 @@ Session, EstimatorV2, SamplerV2, - SamplerV1, - EstimatorV1, Batch, ) from qiskit_ibm_runtime.fake_provider import FakeManila @@ -150,13 +147,13 @@ def cancel_job_safe(job: RuntimeJob, logger: logging.Logger) -> bool: job.cancel() status = job.status() assert ( - status is JobStatus.CANCELLED or status == "CANCELLED" + status == "CANCELLED" ), "cancel() was successful for job {} but its " "status is {}.".format( job.job_id(), status ) return True except RuntimeInvalidStateError: - if job.status() in JOB_FINAL_STATES: + if job.status() in ["DONE", "CANCELLED", "ERROR"]: logger.warning("Unable to cancel job because it's already done.") return False raise @@ -164,8 +161,8 @@ def cancel_job_safe(job: RuntimeJob, logger: logging.Logger) -> bool: def wait_for_status(job, status, poll_time=1, time_out=20): """Wait for job to reach a certain status.""" - wait_time = 1 if status == JobStatus.QUEUED else poll_time - while job.status() not in JOB_FINAL_STATES + (status,) and time_out > 0: + wait_time = 1 if status == "QUEUED" else poll_time + while job.status() not in ["DONE", "CANCELLED", "ERROR"] and time_out > 0: time.sleep(wait_time) time_out -= wait_time if job.status() != status: @@ -454,15 +451,6 @@ def get_primitive_inputs(primitive, backend=None, num_sets=1): elif isinstance(primitive, SamplerV2): circ.measure_all() return {"pubs": [(circ, param_val)] * num_sets} - elif isinstance(primitive, EstimatorV1): - return { - "circuits": [circ] * num_sets, - "observables": [obs] * num_sets, - "parameter_values": [param_val] * num_sets, - } - elif isinstance(primitive, SamplerV1): - circ.measure_all() - return {"circuits": [circ] * num_sets, "parameter_values": [param_val] * num_sets} else: raise ValueError(f"Invalid primitive type {type(primitive)}") From 22b7d65e053f3329281df6a55beb5517dc6375fd Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Wed, 14 Aug 2024 17:18:23 -0400 Subject: [PATCH 04/14] remove pea as experimental (#1865) --- qiskit_ibm_runtime/options/estimator_options.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/qiskit_ibm_runtime/options/estimator_options.py b/qiskit_ibm_runtime/options/estimator_options.py index 5d3d93a24..016821881 100644 --- a/qiskit_ibm_runtime/options/estimator_options.py +++ b/qiskit_ibm_runtime/options/estimator_options.py @@ -98,20 +98,7 @@ class EstimatorOptions(OptionsV2): twirling: Pauli twirling options. See :class:`TwirlingOptions` for all available options. experimental: Experimental options. These options are subject to change without notification, and - stability is not guaranteed. Currently, the available options are: - - * Probabilistic Error Amplification (PEA). To enable PEA, set:: - - estimator_options.experimental = {"resilience": {"zne": {"amplifier": "pea"}}} - - Since PEA is an amplification technique of ZNE, you will also need to enable ZNE by - setting ``resilience.zne_mitigation = True``. - Other documented ``resilience.zne`` options can be used in conjunction to set - extrapolators, amplification levels, etc. Experiments to learn a sparse Pauli noise - model will be performed for every uniquely identified entangling layer - (up to a specified cutoff) in the union of circuits across PUBs; see - :class:~.LayerNoiseLearningOptions` for options. Add full-width barriers to specify - layers unambiguously. + stability is not guaranteed. """ # Sadly we cannot use pydantic's built in validation because it won't work on Unset. From a95fa23cd8006c4e0c8ce63b4bba57afbaf603b6 Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Wed, 14 Aug 2024 17:20:29 -0400 Subject: [PATCH 05/14] doc cleanup (#1866) Co-authored-by: Kevin Tian --- README.md | 2 +- qiskit_ibm_runtime/options/__init__.py | 12 ++---------- qiskit_ibm_runtime/options/environment_options.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9653bf702..872fe5eeb 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ There are several different options you can specify when calling the primitives. ### Primitive versions -Version 2 of the primitives is introduced by `qiskit-ibm-runtime` release 0.21.0. If you are using V1 primitives, refer to [Migrate to the V2 primitives](https://docs.quantum.ibm.com/migration-guides/v2-primitives) on how to migratie to V2 primitives. The examples below all use V2 primitives. +Version 2 of the primitives is introduced by `qiskit-ibm-runtime` release 0.21.0. Version 1 of the primitives is no longer supported. Refer to [Migrate to the V2 primitives](https://docs.quantum.ibm.com/migration-guides/v2-primitives) on how to migratie to V2 primitives. The examples below all use V2 primitives. ### Sampler diff --git a/qiskit_ibm_runtime/options/__init__.py b/qiskit_ibm_runtime/options/__init__.py index c9aa3f938..b46cbc0f9 100644 --- a/qiskit_ibm_runtime/options/__init__.py +++ b/qiskit_ibm_runtime/options/__init__.py @@ -62,8 +62,8 @@ SamplerOptions -Suboptions for V2 primitives only ---------------------------------- +Suboptions +---------- .. autosummary:: :toctree: ../stubs/ @@ -77,14 +77,6 @@ TwirlingOptions ExecutionOptionsV2 SamplerExecutionOptionsV2 - - -Suboptions for both V1 and V2 primitives ----------------------------------------- - -.. autosummary:: - :toctree: ../stubs/ - EnvironmentOptions SimulatorOptions diff --git a/qiskit_ibm_runtime/options/environment_options.py b/qiskit_ibm_runtime/options/environment_options.py index 08eaf2060..0f211de72 100644 --- a/qiskit_ibm_runtime/options/environment_options.py +++ b/qiskit_ibm_runtime/options/environment_options.py @@ -27,7 +27,7 @@ @primitive_dataclass class EnvironmentOptions: - """Options related to the execution environment. This applies to both V1 and V2 primitives. + """Options related to the execution environment. Args: log_level: logging level to set in the execution environment. The valid From 05bb9e14a9225960df064b2cd9b0ced03f925c82 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 14 Aug 2024 18:22:15 -0400 Subject: [PATCH 06/14] Update test_estimator_sampler (#1867) * Cherry pick 0.27.1 release notes into main * Update test_estimator_sampler --- test/integration/test_session.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test/integration/test_session.py b/test/integration/test_session.py index 68729a2cc..355648289 100644 --- a/test/integration/test_session.py +++ b/test/integration/test_session.py @@ -17,7 +17,7 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.primitives import EstimatorResult, SamplerResult +from qiskit.primitives import PrimitiveResult from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import Session, Batch, SamplerV2, EstimatorV2 @@ -35,31 +35,30 @@ class TestIntegrationSession(IBMIntegrationTestCase): def test_estimator_sampler(self, service): """Test calling both estimator and sampler.""" - psi1 = RealAmplitudes(num_qubits=2, reps=2) + backend = service.backend("ibmq_qasm_simulator") + pass_mgr = generate_preset_pass_manager(backend=backend, optimization_level=1) + psi1 = pass_mgr.run(RealAmplitudes(num_qubits=2, reps=2)) # pylint: disable=invalid-name H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) theta1 = [0, 1, 1, 2, 3, 5] - backend = service.backend("ibmq_qasm_simulator") pm = generate_preset_pass_manager(optimization_level=1, target=backend.target) with Session(service, backend=backend) as session: estimator = EstimatorV2(session=session) result = estimator.run([(psi1, H1, [theta1])]).result() - self.assertIsInstance(result, EstimatorResult) + self.assertIsInstance(result, PrimitiveResult) sampler = SamplerV2(session=session) result = sampler.run([pm.run(bell())]).result() - self.assertIsInstance(result, SamplerResult) + self.assertIsInstance(result, PrimitiveResult) result = estimator.run([(psi1, H1, [theta1])]).result() - self.assertIsInstance(result, EstimatorResult) - self.assertEqual(len(result.values), 1) - self.assertEqual(len(result.metadata), 1) - self.assertEqual(result.metadata[0]["shots"], 300) + self.assertIsInstance(result, PrimitiveResult) + self.assertEqual(result[0].metadata["shots"], 4096) result = sampler.run([pm.run(bell())]).result() - self.assertIsInstance(result, SamplerResult) + self.assertIsInstance(result, PrimitiveResult) session.close() @run_integration_test From 99d84c5bcb23c81a92606ef95c7050b02c0e25bd Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Thu, 15 Aug 2024 16:44:30 -0400 Subject: [PATCH 07/14] Exposing `layer_noise_model` option (#1858) Co-authored-by: Christopher J. Wood --- docs/apidocs/index.rst | 1 + docs/apidocs/noise_learner_result.rst | 4 + .../options/resilience_options.py | 10 +- .../utils/noise_learner_result.py | 12 ++- release-notes/unreleased/1858.feat.rst | 3 + test/integration/test_noise_learner.py | 93 +++++++++---------- 6 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 docs/apidocs/noise_learner_result.rst create mode 100644 release-notes/unreleased/1858.feat.rst diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index c8031745a..280a5a7d0 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -9,6 +9,7 @@ qiskit-ibm-runtime API reference runtime_service noise_learner + noise_learner_result options transpiler qiskit_ibm_runtime.transpiler.passes.scheduling diff --git a/docs/apidocs/noise_learner_result.rst b/docs/apidocs/noise_learner_result.rst new file mode 100644 index 000000000..b530306a2 --- /dev/null +++ b/docs/apidocs/noise_learner_result.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.utils.noise_learner_result + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py index 0b22d06da..550b05aca 100644 --- a/qiskit_ibm_runtime/options/resilience_options.py +++ b/qiskit_ibm_runtime/options/resilience_options.py @@ -12,11 +12,12 @@ """Resilience options.""" -from typing import Literal, Union +from typing import List, Literal, Union from dataclasses import asdict from pydantic import model_validator, Field +from ..utils.noise_learner_result import LayerError from .utils import Unset, UnsetType, Dict, primitive_dataclass from .measure_noise_learning_options import MeasureNoiseLearningOptions from .zne_options import ZneOptions @@ -65,6 +66,12 @@ class ResilienceOptionsV2: layer_noise_learning: Layer noise learning options. See :class:`LayerNoiseLearningOptions` for all options. + + layer_noise_model: A list of :class:`LayerError` objects. + If set, all the mitigation strategies that require noise data (e.g., PEC and PEA) + skip the noise learning stage, and instead gather the required information from + ``layer_noise_model``. Layers whose information is missing in ``layer_noise_model`` + are treated as noiseless and their noise is not mitigated. """ measure_mitigation: Union[UnsetType, bool] = Unset @@ -78,6 +85,7 @@ class ResilienceOptionsV2: layer_noise_learning: Union[LayerNoiseLearningOptions, Dict] = Field( default_factory=LayerNoiseLearningOptions ) + layer_noise_model: Union[UnsetType, List[LayerError]] = Unset @model_validator(mode="after") def _validate_options(self) -> "ResilienceOptionsV2": diff --git a/qiskit_ibm_runtime/utils/noise_learner_result.py b/qiskit_ibm_runtime/utils/noise_learner_result.py index e59b6313d..c85259d5f 100644 --- a/qiskit_ibm_runtime/utils/noise_learner_result.py +++ b/qiskit_ibm_runtime/utils/noise_learner_result.py @@ -10,7 +10,17 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""NoiseLearner result class""" +""" +================================================================================== +NoiseLearner result classes (:mod:`qiskit_ibm_runtime.utils.noise_learner_result`) +================================================================================== + +.. autosummary:: + :toctree: ../stubs/ + + PauliLindbladError + LayerError +""" from __future__ import annotations diff --git a/release-notes/unreleased/1858.feat.rst b/release-notes/unreleased/1858.feat.rst new file mode 100644 index 000000000..1aaf41a04 --- /dev/null +++ b/release-notes/unreleased/1858.feat.rst @@ -0,0 +1,3 @@ +``ResilienceOptionsV2`` has a new field ``layer_noise_model``. When this field is set, all the +mitigation strategies that require noise data skip the noise learning stage, and instead gather +the required information from ``layer_noise_model``. \ No newline at end of file diff --git a/test/integration/test_noise_learner.py b/test/integration/test_noise_learner.py index 6a92f2390..ae09d592f 100644 --- a/test/integration/test_noise_learner.py +++ b/test/integration/test_noise_learner.py @@ -17,12 +17,11 @@ from qiskit.circuit import QuantumCircuit from qiskit.providers.jobstatus import JobStatus -from qiskit.compiler import transpile -from qiskit_ibm_runtime import RuntimeJob, Session +from qiskit_ibm_runtime import RuntimeJob, Session, EstimatorV2 from qiskit_ibm_runtime.noise_learner import NoiseLearner from qiskit_ibm_runtime.utils.noise_learner_result import PauliLindbladError, LayerError -from qiskit_ibm_runtime.options import NoiseLearnerOptions +from qiskit_ibm_runtime.options import NoiseLearnerOptions, EstimatorOptions from ..decorators import run_integration_test from ..ibm_test_case import IBMIntegrationTestCase @@ -39,12 +38,12 @@ def setUp(self) -> None: raise SkipTest("test_eagle not available in this environment") c1 = QuantumCircuit(2) - c1.cx(0, 1) + c1.ecr(0, 1) c2 = QuantumCircuit(3) - c2.cx(0, 1) - c2.cx(1, 2) - c2.cx(0, 1) + c2.ecr(0, 1) + c2.ecr(1, 2) + c2.ecr(0, 1) self.circuits = [c1, c2] @@ -65,10 +64,9 @@ def test_with_default_options(self, service): # pylint: disable=unused-argument options = NoiseLearnerOptions() learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) - self._verify(job, self.default_input_options) + self._verify(job, self.default_input_options, 3) @run_integration_test def test_with_non_default_options(self, service): # pylint: disable=unused-argument @@ -80,43 +78,12 @@ def test_with_non_default_options(self, service): # pylint: disable=unused-argu options.layer_pair_depths = [0, 1] learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) input_options = deepcopy(self.default_input_options) input_options["max_layers_to_learn"] = 1 input_options["layer_pair_depths"] = [0, 1] - self._verify(job, input_options) - - @run_integration_test - def test_in_session(self, service): - """Test noise learner when used within a session.""" - backend = self.backend - - options = NoiseLearnerOptions() - options.max_layers_to_learn = 1 - options.layer_pair_depths = [0, 1] - - input_options = deepcopy(self.default_input_options) - input_options["max_layers_to_learn"] = 1 - input_options["layer_pair_depths"] = [0, 1] - - circuits = transpile(self.circuits, backend=backend) - - with Session(service, backend) as session: - options.twirling_strategy = "all" - learner1 = NoiseLearner(mode=session, options=options) - job1 = learner1.run(circuits) - - input_options["twirling_strategy"] = "all" - self._verify(job1, input_options) - - options.twirling_strategy = "active-circuit" - learner2 = NoiseLearner(mode=session, options=options) - job2 = learner2.run(circuits) - - input_options["twirling_strategy"] = "active-circuit" - self._verify(job2, input_options) + self._verify(job, input_options, 1) @run_integration_test def test_with_no_layers(self, service): # pylint: disable=unused-argument @@ -127,20 +94,52 @@ def test_with_no_layers(self, service): # pylint: disable=unused-argument options.max_layers_to_learn = 0 learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) self.assertEqual(job.result().data, []) input_options = deepcopy(self.default_input_options) input_options["max_layers_to_learn"] = 0 - self._verify(job, input_options) + self._verify(job, input_options, 0) + + @run_integration_test + def test_learner_plus_estimator(self, service): # pylint: disable=unused-argument + """Test feeding noise learner data to estimator.""" + backend = self.backend + + options = EstimatorOptions() + options.resilience.zne_mitigation = True # pylint: disable=assigning-non-slot + options.resilience.zne.amplifier = "pea" + options.resilience.layer_noise_learning.layer_pair_depths = [0, 1] - def _verify(self, job: RuntimeJob, expected_input_options: dict) -> None: + pubs = [(c, "Z" * c.num_qubits) for c in self.circuits] + + with Session(service, backend) as session: + learner = NoiseLearner(mode=session, options=options) + learner_job = learner.run(self.circuits) + noise_model = learner_job.result() + self.assertEqual(len(noise_model), 3) + + estimator = EstimatorV2(mode=session, options=options) + estimator.options.resilience.layer_noise_model = noise_model + + estimator_job = estimator.run(pubs) + result = estimator_job.result() + + noise_model_metadata = result.metadata["resilience"]["layer_noise_model"] + for x, y in zip(noise_model, noise_model_metadata): + self.assertEqual(x.circuit, y.circuit) + self.assertEqual(x.qubits, y.qubits) + self.assertEqual(x.error.generators, y.error.generators) + self.assertEqual(x.error.rates.tolist(), y.error.rates.tolist()) + + def _verify(self, job: RuntimeJob, expected_input_options: dict, n_results: int) -> None: job.wait_for_final_state() self.assertEqual(job.status(), JobStatus.DONE, job.error_message()) result = job.result() + self.assertEqual(len(result), n_results) + for datum in result.data: circuit = datum.circuit qubits = datum.qubits From cfaae78556962c430de86c147b864b35a0d8f777 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Thu, 15 Aug 2024 16:49:33 -0400 Subject: [PATCH 08/14] Remove session id "service" param deprecation (#1868) * Remove session id deprecation * Add release note --- qiskit_ibm_runtime/session.py | 18 +----------------- release-notes/unreleased/1868.other.rst | 1 + test/integration/test_session.py | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) create mode 100644 release-notes/unreleased/1868.other.rst diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index d2137afc4..46efe039d 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -17,7 +17,6 @@ from typing import Dict, Optional, Type, Union, Callable, Any from types import TracebackType from functools import wraps -import warnings from qiskit.providers.backend import BackendV1, BackendV2 @@ -351,18 +350,13 @@ def service(self) -> QiskitRuntimeService: return self._service @classmethod - def from_id( - cls, - session_id: str, - service: Optional[QiskitRuntimeService] = None, - ) -> "Session": + def from_id(cls, session_id: str, service: QiskitRuntimeService) -> "Session": """Construct a Session object with a given session_id Args: session_id: the id of the session to be created. This must be an already existing session id. service: instance of the ``QiskitRuntimeService`` class. - If ``None``, ``QiskitRuntimeService()`` is used to initialize your default saved account. Raises: IBMInputValueError: If given `session_id` does not exist. @@ -371,16 +365,6 @@ def from_id( A new Session with the given ``session_id`` """ - if not service: - warnings.warn( - ( - "The `service` parameter will be required in a future release no sooner than " - "3 months after the release of qiskit-ibm-runtime 0.23.0 ." - ), - DeprecationWarning, - stacklevel=2, - ) - service = QiskitRuntimeService() response = service._api_client.session_details(session_id) backend = response.get("backend_name") diff --git a/release-notes/unreleased/1868.other.rst b/release-notes/unreleased/1868.other.rst new file mode 100644 index 000000000..c3edaf1ab --- /dev/null +++ b/release-notes/unreleased/1868.other.rst @@ -0,0 +1 @@ +The ``service`` parameter is now required in ``Session.from_id()``. \ No newline at end of file diff --git a/test/integration/test_session.py b/test/integration/test_session.py index 355648289..79d7c8c7c 100644 --- a/test/integration/test_session.py +++ b/test/integration/test_session.py @@ -82,7 +82,7 @@ def test_session_from_id(self, service): except: raise SkipTest("No proper backends available") pm = generate_preset_pass_manager(backend=backend, optimization_level=1) - isa_circuit = pm.run(bell()) + isa_circuit = pm.run([bell()]) with Session(service, backend=backend) as session: sampler = SamplerV2(session=session) sampler.run(isa_circuit) From 2e5e6aac350b3b5b4c49e0fdb1ce399cab79dab1 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Thu, 15 Aug 2024 17:05:36 -0400 Subject: [PATCH 09/14] Prepare release 0.28.0 (#1869) * Prepare release 0.28.0 * Docs build --- release-notes/0.28.0.rst | 17 +++++++++++++++++ release-notes/unreleased/1857.other.rst | 2 -- release-notes/unreleased/1858.feat.rst | 3 --- release-notes/unreleased/1868.other.rst | 1 - 4 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 release-notes/0.28.0.rst delete mode 100644 release-notes/unreleased/1857.other.rst delete mode 100644 release-notes/unreleased/1858.feat.rst delete mode 100644 release-notes/unreleased/1868.other.rst diff --git a/release-notes/0.28.0.rst b/release-notes/0.28.0.rst new file mode 100644 index 000000000..1b1c8994e --- /dev/null +++ b/release-notes/0.28.0.rst @@ -0,0 +1,17 @@ +0.28.0 (2024-08-15) +=================== + +New Features +------------ + +- ``ResilienceOptionsV2`` has a new field ``layer_noise_model``. When this field is set, all the + mitigation strategies that require noise data skip the noise learning stage, and instead gather + the required information from ``layer_noise_model``. (`1858 `__) + + +Other Notes +----------- + +- The V1 Primitives ``SamplerV1`` and ``EstimatorV1`` have been completely removed. Please use the + `migration guide `__ and use theV2 Primitives instead. (`1857 `__) +- The ``service`` parameter is now required in ``Session.from_id()``. (`1868 `__) diff --git a/release-notes/unreleased/1857.other.rst b/release-notes/unreleased/1857.other.rst deleted file mode 100644 index 4d5c60097..000000000 --- a/release-notes/unreleased/1857.other.rst +++ /dev/null @@ -1,2 +0,0 @@ -The V1 Primitives ``SamplerV1`` and ``EstimatorV1`` have been completely removed. Please use the -V2 Primitives instead https://docs.quantum.ibm.com/migration-guides/v2-primitives. \ No newline at end of file diff --git a/release-notes/unreleased/1858.feat.rst b/release-notes/unreleased/1858.feat.rst deleted file mode 100644 index 1aaf41a04..000000000 --- a/release-notes/unreleased/1858.feat.rst +++ /dev/null @@ -1,3 +0,0 @@ -``ResilienceOptionsV2`` has a new field ``layer_noise_model``. When this field is set, all the -mitigation strategies that require noise data skip the noise learning stage, and instead gather -the required information from ``layer_noise_model``. \ No newline at end of file diff --git a/release-notes/unreleased/1868.other.rst b/release-notes/unreleased/1868.other.rst deleted file mode 100644 index c3edaf1ab..000000000 --- a/release-notes/unreleased/1868.other.rst +++ /dev/null @@ -1 +0,0 @@ -The ``service`` parameter is now required in ``Session.from_id()``. \ No newline at end of file From 474d7cd526a1e6e8c2229893614d37e316127307 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:41:26 -0400 Subject: [PATCH 10/14] Add back alias docs pages for Sampler and Estimator (#1872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This results in a dedicated docs page being created like below. This is useful to avoid 404ing the page and to make extra-explicit that Estimator is now an alias for EstimatorV2. Screenshot 2024-08-16 at 9 30 32 AM --- qiskit_ibm_runtime/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index a82c5c419..80c200a90 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -185,7 +185,9 @@ :toctree: ../stubs/ QiskitRuntimeService + Estimator EstimatorV2 + Sampler SamplerV2 Session Batch From 839d7f178274d22da476b6efeb61e885d38650b2 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 16 Aug 2024 10:38:54 -0400 Subject: [PATCH 11/14] Update main branch 0.29.0 (#1870) --- docs/conf.py | 2 +- qiskit_ibm_runtime/VERSION.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d656b0ad3..273ff589e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.28.0' +release = '0.29.0' # -- General configuration --------------------------------------------------- diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index 697f087f3..ae6dd4e20 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.28.0 +0.29.0 From 157233c731433067f6e2860b7795d8fd14efcf0d Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:13:07 -0400 Subject: [PATCH 12/14] Fix 404 link in release note (#1876) --- release-notes/0.12.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/0.12.1.rst b/release-notes/0.12.1.rst index 86d04c4a4..56881e2ec 100644 --- a/release-notes/0.12.1.rst +++ b/release-notes/0.12.1.rst @@ -31,7 +31,7 @@ Bug Fixes a past date. - The ``noise_factors`` and ``extrapolator`` options in - `qiskit_ibm_runtime.options.ResilienceOptions `__ + `qiskit_ibm_runtime.options.ResilienceOptions `__ will now default to ``None`` unless ``resilience_level`` is set to 2. Only options relevant to the resilience level will be set, so when using ``resilience_level`` 2, ``noise_factors`` will still default to From e3b663291f3a64f2bf9d81e2d847809d58303746 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Mon, 19 Aug 2024 11:10:21 -0400 Subject: [PATCH 13/14] Update integration tests (#1875) * Integration tests * test * don't test v1 backends * revert previous changes --- test/integration/test_fake_backends.py | 29 +++----------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/test/integration/test_fake_backends.py b/test/integration/test_fake_backends.py index dd255371d..2532dd5ab 100644 --- a/test/integration/test_fake_backends.py +++ b/test/integration/test_fake_backends.py @@ -78,30 +78,7 @@ def test_circuit_on_fake_backend_v2(self, backend, optimization_level): max_count = max(counts.items(), key=operator.itemgetter(1))[0] self.assertEqual(max_count, "11") - @idata( - itertools.product( - [be for be in FAKE_PROVIDER.backends() if be.configuration().num_qubits > 1], - [0, 1, 2, 3], - ) - ) - @unpack - def test_circuit_on_fake_backend(self, backend, optimization_level): - if not optionals.HAS_AER and backend.configuration().num_qubits > 20: - self.skipTest( - "Unable to run fake_backend %s without qiskit-aer" - % backend.configuration().backend_name - ) - backend.set_options(seed_simulator=42) - pm = generate_preset_pass_manager(backend=backend, optimization_level=optimization_level) - isa_circuit = pm.run(self.circuit) - sampler = Sampler(backend) - job = sampler.run([isa_circuit]) - pub_result = job.result()[0] - counts = pub_result.data.meas.get_counts() - max_count = max(counts.items(), key=operator.itemgetter(1))[0] - self.assertEqual(max_count, "11") - - @data(*FAKE_PROVIDER.backends(), *FAKE_PROVIDER_FOR_BACKEND_V2.backends()) + @data(*FAKE_PROVIDER_FOR_BACKEND_V2.backends()) def test_to_dict_properties(self, backend): properties = backend.properties() if properties: @@ -120,7 +97,7 @@ def test_backend_v2_dtm(self, backend): if backend.dtm: self.assertLess(backend.dtm, 1e-6) - @data(*FAKE_PROVIDER.backends(), *FAKE_PROVIDER_FOR_BACKEND_V2.backends()) + @data(*FAKE_PROVIDER_FOR_BACKEND_V2.backends()) def test_to_dict_configuration(self, backend): configuration = backend.configuration() if configuration.open_pulse: @@ -142,7 +119,7 @@ def test_to_dict_configuration(self, backend): self.assertIsInstance(configuration.to_dict(), dict) - @data(*FAKE_PROVIDER.backends(), *FAKE_PROVIDER_FOR_BACKEND_V2.backends()) + @data(*FAKE_PROVIDER_FOR_BACKEND_V2.backends()) def test_defaults_to_dict(self, backend): if hasattr(backend, "defaults"): defaults = backend.defaults() From 848b6ac1c82392d6593c4dd2ad9ad33f52d19b1b Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Mon, 19 Aug 2024 14:41:32 -0400 Subject: [PATCH 14/14] Minor release note typo (#1878) --- release-notes/0.28.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-notes/0.28.0.rst b/release-notes/0.28.0.rst index 1b1c8994e..1af0a8914 100644 --- a/release-notes/0.28.0.rst +++ b/release-notes/0.28.0.rst @@ -12,6 +12,6 @@ New Features Other Notes ----------- -- The V1 Primitives ``SamplerV1`` and ``EstimatorV1`` have been completely removed. Please use the - `migration guide `__ and use theV2 Primitives instead. (`1857 `__) +- The V1 Primitives ``SamplerV1`` and ``EstimatorV1`` have been completely removed. Please see the + `migration guide `__ and use the V2 Primitives instead. (`1857 `__) - The ``service`` parameter is now required in ``Session.from_id()``. (`1868 `__)