diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 504079584..e3ddf322d 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -34,15 +34,15 @@ from .runtime_job_v2 import RuntimeJobV2 from .ibm_backend import IBMBackend from .utils.default_session import get_cm_session -from .utils.deprecation import issue_deprecation_msg +from .utils.deprecation import issue_deprecation_msg, deprecate_function from .utils.utils import validate_isa_circuits, is_simulator, validate_no_dd_with_dynamic_circuits from .constants import DEFAULT_DECODERS from .qiskit_runtime_service import QiskitRuntimeService from .fake_provider.local_service import QiskitRuntimeLocalService - # pylint: disable=unused-import,cyclic-import from .session import Session +from .batch import Batch logger = logging.getLogger(__name__) OptionsT = TypeVar("OptionsT", bound=BaseOptions) @@ -56,24 +56,17 @@ class BasePrimitiveV2(ABC, Generic[OptionsT]): def __init__( self, - backend: Optional[Union[str, BackendV1, BackendV2]] = None, - session: Optional[Session] = None, + mode: Optional[Union[BackendV1, BackendV2, Session, Batch, str]] = None, options: Optional[Union[Dict, OptionsT]] = None, ): """Initializes the primitive. Args: + mode: The execution mode used to make the primitive query. It can be - 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. + * A :class:`Backend` if you are using job mode. + * A :class:`Session` if you are using session execution mode. + * A :class:`Batch` if you are using batch execution mode. options: Primitive options, see :class:`qiskit_ibm_runtime.options.EstimatorOptions` and :class:`qiskit_ibm_runtime.options.SamplerOptions` for detailed description @@ -82,38 +75,36 @@ def __init__( Raises: ValueError: Invalid arguments are given. """ - self._session: Optional[Session] = None + self._mode: Optional[Union[Session, Batch]] = None self._service: QiskitRuntimeService | QiskitRuntimeLocalService = None self._backend: Optional[BackendV1 | BackendV2] = None self._set_options(options) - 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)): + if isinstance(mode, (Session, Batch)): + self._mode = mode + self._service = self._mode.service + self._backend = self._mode._backend + elif isinstance(mode, IBMBackend): # type: ignore[unreachable] + self._service = mode.service + self._backend = mode + elif isinstance(mode, (BackendV1, BackendV2)): self._service = QiskitRuntimeLocalService() - self._backend = backend - elif isinstance(backend, str): + self._backend = mode + elif isinstance(mode, str): self._service = ( QiskitRuntimeService() if QiskitRuntimeService.global_service is None else QiskitRuntimeService.global_service ) - self._backend = self._service.backend(backend) + self._backend = self._service.backend(mode) + elif mode is not None: # type: ignore[unreachable] + raise ValueError("mode must be of type Backend, Session, Batch or None") 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 + self._mode = get_cm_session() + self._service = self._mode.service + self._backend = self._service.backend( # type: ignore + name=self._mode.backend(), instance=self._mode._instance ) else: self._service = ( @@ -159,8 +150,9 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV logger.info("Submitting job using options %s", primitive_options) - if self._session: - return self._session.run( + # Batch or Session + if self._mode: + return self._mode.run( program_id=self._program_id(), inputs=primitive_inputs, options=runtime_options, @@ -195,7 +187,17 @@ def session(self) -> Optional[Session]: Returns: Session used by this primitive, or ``None`` if session is not used. """ - return self._session + deprecate_function("session", "0.23.0", "Please use the 'mode' property instead.") + return self._mode + + @property + def mode(self) -> Optional[Session | Batch]: + """Return the execution mode used by this primitive. + + Returns: + Mode used by this primitive, or ``None`` if an execution mode is not used. + """ + return self._mode @property def options(self) -> OptionsT: diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 6de38baa5..68da42347 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -18,24 +18,28 @@ 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 .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 # pylint: disable=unused-import,cyclic-import from .session import Session +from .batch import Batch logger = logging.getLogger(__name__) @@ -104,13 +108,23 @@ class EstimatorV2(BasePrimitiveV2[EstimatorOptions], Estimator, BaseEstimatorV2) def __init__( self, - backend: Optional[Union[str, IBMBackend]] = None, + mode: Optional[Union[BackendV1, BackendV2, Session, Batch, str]] = None, + backend: Optional[Union[str, BackendV1, BackendV2]] = None, session: Optional[Session] = None, options: Optional[Union[Dict, EstimatorOptions]] = None, ): """Initializes the Estimator primitive. Args: + mode: The execution mode used to make the primitive query. It can be: + + * A :class:`Backend` if you are using job mode. + * A :class:`Session` if you are using session execution mode. + * A :class:`Batch` if you are using batch execution mode. + + Refer to the `Qiskit Runtime documentation `_. + for more information about the ``Execution modes``. + 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. @@ -129,7 +143,29 @@ def __init__( """ BaseEstimatorV2.__init__(self) Estimator.__init__(self) - BasePrimitiveV2.__init__(self, backend=backend, session=session, options=options) + if backend: + deprecate_arguments( + "backend", + "0.23.0", + "Please use the 'mode' parameter instead.", + ) + if session: + deprecate_arguments( + "session", + "0.23.0", + "Please use the 'mode' parameter instead.", + ) + if isinstance(mode, str) or isinstance(backend, str): + issue_deprecation_msg( + "The backend name as execution mode input has been deprecated.", + "0.23.0", + "A backend object should be provided instead. Get the backend directly from" + " the service using `QiskitRuntimeService().backend('ibm_backend')`", + 3, + ) + if mode is None: + mode = session if backend and session else backend if backend else session + BasePrimitiveV2.__init__(self, mode=mode, options=options) def run( self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py index d705fa566..9e8ed4ff1 100644 --- a/qiskit_ibm_runtime/sampler.py +++ b/qiskit_ibm_runtime/sampler.py @@ -22,6 +22,7 @@ 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 @@ -31,6 +32,8 @@ # 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 .options import SamplerOptions @@ -63,13 +66,23 @@ class SamplerV2(BasePrimitiveV2[SamplerOptions], Sampler, BaseSamplerV2): def __init__( self, - backend: Optional[Union[str, IBMBackend]] = None, + mode: Optional[Union[BackendV1, BackendV2, Session, Batch, str]] = None, + backend: Optional[Union[str, BackendV1, BackendV2]] = None, session: Optional[Session] = None, options: Optional[Union[Dict, SamplerOptions]] = None, ): """Initializes the Sampler primitive. Args: + mode: The execution mode used to make the primitive query. It can be: + + * A :class:`Backend` if you are using job mode. + * A :class:`Session` if you are using session execution mode. + * A :class:`Batch` if you are using batch execution mode. + + Refer to the `Qiskit Runtime documentation `_. + for more information about the ``Execution modes``. + 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. @@ -89,7 +102,29 @@ def __init__( self.options: SamplerOptions BaseSamplerV2.__init__(self) Sampler.__init__(self) - BasePrimitiveV2.__init__(self, backend=backend, session=session, options=options) + if backend: + deprecate_arguments( + "backend", + "0.23.0", + "Please use the 'mode' parameter instead.", + ) + if session: + deprecate_arguments( + "session", + "0.23.0", + "Please use the 'mode' parameter instead.", + ) + if isinstance(mode, str) or isinstance(backend, str): + issue_deprecation_msg( + "The backend name as execution mode input has been deprecated.", + "0.23.0", + "A backend object should be provided instead. Get the backend directly from" + " the service using `QiskitRuntimeService().backend('ibm_backend')`", + 3, + ) + if mode is None: + mode = session if backend and session else backend if backend else session + BasePrimitiveV2.__init__(self, mode=mode, options=options) def run(self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None) -> RuntimeJobV2: """Submit a request to the sampler primitive. diff --git a/release-notes/unreleased/1556.deprecation.rst b/release-notes/unreleased/1556.deprecation.rst new file mode 100644 index 000000000..fd35f9abb --- /dev/null +++ b/release-notes/unreleased/1556.deprecation.rst @@ -0,0 +1,2 @@ +`backend` argument in `Sampler `__ and `Estimator `__ has been deprecated . Please use the new `mode` argument instead. +`session` argument in `Sampler `__ and `Estimator `__ has been deprecated . Please use the new `mode` argument instead. diff --git a/release-notes/unreleased/1556.feat.rst b/release-notes/unreleased/1556.feat.rst new file mode 100644 index 000000000..2950a3149 --- /dev/null +++ b/release-notes/unreleased/1556.feat.rst @@ -0,0 +1,3 @@ +Related to the execution modes, Sampler and Estimator now include a `mode` argument. The `mode` param +can be a Backend, Session, Batch, or None. Due to this, the backend name has been deprecated, and will +no longer be supported as a valid execution mode. diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index ede5525db..f60d15521 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -42,6 +42,7 @@ get_mocked_backend, bell, get_mocked_session, + get_mocked_batch, ) @@ -141,22 +142,19 @@ def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument with patch("qiskit_ibm_runtime.base_primitive.QiskitRuntimeService", new=MockQRTService): inst = primitive(backend=backend_name) - self.assertIsNone(inst.session) + self.assertIsNone(inst.mode) inst.run(**get_primitive_inputs(inst)) mock_service_inst.run.assert_called_once() runtime_options = mock_service_inst.run.call_args.kwargs["options"] self.assertEqual(runtime_options["backend"], mock_backend) - @data(EstimatorV2, SamplerV2) - def test_init_with_session_backend_str(self, primitive): - """Test initializing a primitive with a backend name using session.""" - backend_name = "ibm_gotham" - - with patch("qiskit_ibm_runtime.base_primitive.QiskitRuntimeService"): - with self.assertRaises(ValueError) as exc: - inst = primitive(session=backend_name) - self.assertIsNone(inst.session) - self.assertIn("session must be of type Session or None", str(exc.exception)) + mock_service_inst.reset_mock() + str_mode_inst = primitive(mode=backend_name) + self.assertIsNone(str_mode_inst.mode) + inst.run(**get_primitive_inputs(str_mode_inst)) + mock_service_inst.run.assert_called_once() + runtime_options = mock_service_inst.run.call_args.kwargs["options"] + self.assertEqual(runtime_options["backend"], mock_backend) @data(EstimatorV2, SamplerV2) def test_init_with_backend_instance(self, primitive): @@ -166,17 +164,12 @@ def test_init_with_backend_instance(self, primitive): service.reset_mock() inst = primitive(backend=backend) - self.assertIsNone(inst.session) + self.assertIsNone(inst.mode) 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 = primitive(session=backend) - self.assertIsNone(inst.session) - self.assertIn("session must be of type Session or None", str(exc.exception)) - @data(EstimatorV2, SamplerV2) def test_init_with_backend_session(self, primitive): """Test initializing a primitive with both backend and session.""" @@ -184,8 +177,8 @@ def test_init_with_backend_session(self, primitive): session = get_mocked_session(get_mocked_backend(backend_name)) session.reset_mock() - inst = primitive(session=session, backend=backend_name) - self.assertIsNotNone(inst.session) + inst = primitive(session=session) + self.assertIsNotNone(inst.mode) inst.run(**get_primitive_inputs(inst)) session.run.assert_called_once() @@ -200,7 +193,7 @@ def test_init_with_no_backend_session_cloud(self, primitive): mock_service.global_service = None inst = primitive() mock_service.assert_called_once() - self.assertIsNone(inst.session) + self.assertIsNone(inst.mode) @data(EstimatorV2, SamplerV2) def test_init_with_no_backend_session_quantum(self, primitive): @@ -219,8 +212,8 @@ def test_default_session_context_manager(self, primitive): with Session(service=backend.service, backend=backend_name) as session: inst = primitive() - self.assertEqual(inst.session, session) - self.assertEqual(inst.session.backend(), backend_name) + self.assertEqual(inst.mode, session) + self.assertEqual(inst.mode.backend(), backend_name) @data(EstimatorV2, SamplerV2) def test_default_session_cm_new_backend(self, primitive): @@ -231,7 +224,7 @@ def test_default_session_cm_new_backend(self, primitive): with Session(service=service, backend=cm_backend): inst = primitive(backend=backend) - self.assertIsNone(inst.session) + self.assertIsNone(inst.mode) inst.run(**get_primitive_inputs(inst)) service.run.assert_called_once() runtime_options = service.run.call_args.kwargs["options"] @@ -244,12 +237,54 @@ def test_no_session(self, primitive): service = backend.service inst = primitive(backend) inst.run(**get_primitive_inputs(inst)) - self.assertIsNone(inst.session) + self.assertIsNone(inst.mode) service.run.assert_called_once() kwargs_list = service.run.call_args.kwargs self.assertNotIn("session_id", kwargs_list) self.assertNotIn("start_session", kwargs_list) + @data(SamplerV2, EstimatorV2) + def test_init_with_mode_as_backend(self, primitive): + """Test initializing a primitive with mode as a Backend.""" + backend = get_mocked_backend() + service = backend.service + + inst = primitive(mode=backend) + self.assertIsNotNone(inst) + 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) + + @data(SamplerV2, EstimatorV2) + def test_init_with_mode_as_session(self, primitive): + """Test initializing a primitive with mode as Session.""" + backend = get_mocked_backend() + session = get_mocked_session(backend) + session.reset_mock() + session._backend = backend + + inst = primitive(mode=session) + self.assertIsNotNone(inst.mode) + inst.run(**get_primitive_inputs(inst, backend=backend)) + self.assertEqual(inst.mode, session) + session.run.assert_called_once() + self.assertEqual(session._backend, backend) + + @data(SamplerV2, EstimatorV2) + def test_init_with_mode_as_batch(self, primitive): + """Test initializing a primitive with mode as a Batch""" + backend = get_mocked_backend() + batch = get_mocked_batch(backend) + batch.reset_mock() + batch._backend = backend + + inst = primitive(mode=batch) + self.assertIsNotNone(inst.mode) + inst.run(**get_primitive_inputs(inst, backend=backend)) + batch.run.assert_called_once() + self.assertEqual(batch._backend, backend) + @data(EstimatorV2, SamplerV2) def test_parameters_single_circuit(self, primitive): """Test parameters for a single cirucit.""" diff --git a/test/unit/test_local_mode.py b/test/unit/test_local_mode.py index 0d5203b07..ade45d7ab 100644 --- a/test/unit/test_local_mode.py +++ b/test/unit/test_local_mode.py @@ -180,7 +180,7 @@ def test_v2_estimator_with_accepted_options(self, backend): @combine( primitive=[SamplerV2, EstimatorV2], backend=[FakeManila(), FakeManilaV2(), AerSimulator()] ) - def test_primitve_v2_with_not_accepted_options(self, primitive, backend): + def test_primitive_v2_with_not_accepted_options(self, primitive, backend): """Test V1 primitive with accepted options.""" options = { "max_execution_time": 200, @@ -223,7 +223,7 @@ def test_estimator_v2_session(self, session_cls, backend): self.assertIsInstance(pub_result.metadata, dict) @data(FakeManila(), FakeManilaV2(), AerSimulator()) - def test_non_primitve(self, backend): + def test_non_primitive(self, backend): """Test calling non-primitive in local mode.""" session = Session(backend=backend) with self.assertRaisesRegex(ValueError, "Only sampler and estimator"): diff --git a/test/utils.py b/test/utils.py index c4531b149..547d460b4 100644 --- a/test/utils.py +++ b/test/utils.py @@ -40,6 +40,7 @@ SamplerV2, SamplerV1, EstimatorV1, + Batch, ) from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime.hub_group_project import HubGroupProject @@ -332,6 +333,15 @@ def get_mocked_session(backend: Any = None) -> mock.MagicMock: return session +def get_mocked_batch(backend: Any = None) -> mock.MagicMock: + """Return a mocked batch object.""" + batch = mock.MagicMock(spec=Batch) + batch._instance = None + batch._backend = backend or get_mocked_backend() + batch._service = getattr(backend, "service", None) or mock.MagicMock(spec=QiskitRuntimeService) + return batch + + def submit_and_cancel(backend: IBMBackend, logger: logging.Logger) -> RuntimeJob: """Submit and cancel a job.